Compare commits

..

422 Commits

Author SHA1 Message Date
Greyson Parrelli
6ca8218d55 Bump version to 4.51.6 2019-12-06 11:58:12 -05:00
Greyson Parrelli
653afbdd46 Updated language translations. 2019-12-06 11:57:33 -05:00
Alex Hart
4453d1752f Fix reactions scrubber positioning on vertically split multiscreen. 2019-12-06 11:43:51 -05:00
Alan Evans
5782c8a58b Make media overview view pager scrollable for long translations. 2019-12-06 11:43:51 -05:00
Greyson Parrelli
3e041befc8 Bump version to 4.51.5 2019-12-06 01:14:24 -05:00
Greyson Parrelli
6d3847c26f Updated language translations. 2019-12-06 01:10:47 -05:00
Alex Hart
4bdfc37bc1 Fix swipe to reply clobbering long press events. 2019-12-06 01:00:39 -05:00
Alan Evans
28afe0d829 Fix first media overview item selected text. 2019-12-06 01:00:39 -05:00
Greyson Parrelli
04dddd3378 Bump version to 4.51.4 2019-12-05 19:15:10 -05:00
Greyson Parrelli
23a14583fe Updated language translations. 2019-12-05 19:14:48 -05:00
Greyson Parrelli
19c83de510 Fix possible performance issues with reactions query. 2019-12-05 19:10:37 -05:00
Greyson Parrelli
e94d4b64cf Bump version to 4.51.3 2019-12-05 12:21:12 -05:00
Greyson Parrelli
7380c99f3a Updated language translations. 2019-12-05 12:20:28 -05:00
Greyson Parrelli
2961a372c3 Avoid unneccessary recipient refreshes. 2019-12-05 12:20:28 -05:00
Greyson Parrelli
5e2a4fb058 Put safeguards around Recipient creation in the IdentityStore. 2019-12-05 12:20:28 -05:00
Greyson Parrelli
a16242b9f8 Fix issues with RecipientDatabase#update(). 2019-12-05 12:20:28 -05:00
Alan Evans
acfba9ac96 Show total file size of selected items when appropriate. 2019-12-05 12:20:28 -05:00
Alan Evans
006343460e Fix long press on image thumbnail in detail view.
Allow long press to select inside select mode.
2019-12-05 12:20:28 -05:00
Alex Hart
1cc8634cc7 Display reactions in ImageView instead of TextView on message bubbles. 2019-12-05 12:20:27 -05:00
Greyson Parrelli
9f8a110428 Bump version to 4.51.2 2019-12-05 02:59:42 -05:00
Greyson Parrelli
07b4279d0b Updated language translations. 2019-12-05 02:59:42 -05:00
Greyson Parrelli
6a33b231e3 Add a FrameRateTracker to log frame drops. 2019-12-05 02:59:42 -05:00
Greyson Parrelli
b38d02061d Improve logging in RetrieveProfileJob. 2019-12-05 02:59:42 -05:00
Greyson Parrelli
f832a36a5e Prevent possible UUID-only recipient creations. 2019-12-05 02:59:42 -05:00
Alan Evans
911ca7c29d Improve storage management detail view descriptions. 2019-12-05 02:59:42 -05:00
Greyson Parrelli
544b75a2a7 Bump version to 4.51.2 2019-12-04 15:30:30 -05:00
Greyson Parrelli
56e8d4fb06 Updated language translations. 2019-12-04 15:28:45 -05:00
Greyson Parrelli
36a2278aef Add sanity checks for phone number during link process. 2019-12-04 15:28:45 -05:00
Alex Hart
0c785b85b8 Fix video icon issue. 2019-12-04 15:28:45 -05:00
Alan Evans
a0ecba147e Use decimal digit groups in file size pretty printing. 2019-12-04 15:28:45 -05:00
Alan Evans
977591ac82 Remove assertion error in recipient preferences. 2019-12-04 15:28:28 -05:00
Greyson Parrelli
fe1838d3fe Fix issue where unlocking would dismiss MainActivity. 2019-12-04 15:25:26 -05:00
Greyson Parrelli
ede06cf97d Remove unnecessary directory refreshes. 2019-12-04 15:25:26 -05:00
Greyson Parrelli
93deee6824 Attempt to fix crash in ClassicOpenHelper.
These users must have > 1.5-year-old installs that haven't been updated,
but it's still a top crash for whatever reason, so gotta try to fix it.
2019-12-04 15:25:26 -05:00
Greyson Parrelli
db19077834 Fix crash when receiving a PreKey message in a new session.
We don't allow creating recipients with only a UUID at the moment
(for good reason), but the way the decrypt method was written, it
was possible to do so. Until we have CDS, we should prefer the phone
number in scenarios like these.
2019-12-04 15:25:26 -05:00
Alex Hart
6f91f62db2 Fix InviteActivity sendSmsMessage type error. 2019-12-04 15:25:26 -05:00
Greyson Parrelli
1fc63b7597 Fix crash in conversation search. 2019-12-04 15:25:26 -05:00
Alex Hart
a079e479ec Skip call dialog if signal call is already active. 2019-12-04 15:25:26 -05:00
Alex Hart
e90705b459 Add correct archive icon to light theme. 2019-12-04 15:25:26 -05:00
Alex Hart
244db437cb Fix reaction display issues. 2019-12-04 15:25:25 -05:00
Alan Evans
6558eae032 Fix audio color on light theme. 2019-12-04 09:10:48 -05:00
Greyson Parrelli
88b54a262b Bump version to 4.51.0 2019-12-04 00:17:03 -05:00
Alan Evans
4fa8b8a4bd Updated language translations. 2019-12-04 00:08:01 -05:00
Greyson Parrelli
cc0ced9a81 Add internal pre-alpha support for storage service. 2019-12-04 00:08:01 -05:00
Alan Evans
52447f5e97 Add storage management features. 2019-12-04 00:07:49 -05:00
Alex Hart
bceb69b284 Add internal pre-alpha support for receiving reactions. 2019-12-04 00:07:49 -05:00
Alex Hart
a8d826020d Implement new video call experience. 2019-12-04 00:07:49 -05:00
Alan Evans
2ada7f87f2 Update lib phone number. 2019-12-04 00:07:49 -05:00
Alan Evans
7f8ca58762 Add internal pre-alpha support for Registration Lock v2. 2019-12-04 00:07:49 -05:00
Greyson Parrelli
058c25808b Clean up website auto-update APKs.
I think previously we thought that because we always used the same
filename, that we were just overwring the same file repeatedly. However,
DownloadManager will actually append numbers to a filename instead of
overwriting, so we were generating a ton of files.

Now we just clear out any previous files before downloading a new one.

Related to #9033
2019-12-04 00:07:49 -05:00
Greyson Parrelli
c1e6b6b086 Add better Loader performance logs. 2019-12-04 00:07:49 -05:00
Greyson Parrelli
b0a6bb79f6 Fix issue with setting Note to Self color.
Fixes #9235
2019-12-04 00:07:49 -05:00
Greyson Parrelli
de52bf50a2 Improve image and sticker notifications. 2019-12-04 00:07:49 -05:00
Greyson Parrelli
207dd23c86 Fix performance issue with large number of notifications.
Constructing the notification would call KeyCachingService#isLocked()
many times in a loop. This call is slow, because when the device isn't
locked, it would construct the master secret each time, which can take
50+ ms. Do that twice in a loop a hundred times, and it adds up.

This simplified #isLocked to avoid the unnecessary master secret
generation.
2019-12-04 00:07:49 -05:00
Alex Hart
7e0de29dd7 Add notification policy permission and runtime check.
Fixes #9217
2019-12-04 00:07:49 -05:00
Greyson Parrelli
eaa8f1ee8f Ignore expiration updates from groups you've left. 2019-12-04 00:07:49 -05:00
Alex Hart
fb494c1151 Implement new invite screen. 2019-12-04 00:07:49 -05:00
Greyson Parrelli
49ecd9ef5d Added additional logging in RetrieveProfileJob. 2019-12-04 00:07:49 -05:00
Greyson Parrelli
8772214fd4 Increase number of log files to 7. 2019-12-04 00:07:49 -05:00
Greyson Parrelli
5b74d0cbeb Improve activity lifecycle logging. 2019-12-04 00:07:49 -05:00
Greyson Parrelli
1e7c93007d Convert the conversation list into a real fragment.
It was a fragment before, but it's functionality was inappropriately
split between the various layers.

This also sets us up better to do tablet stuff in the future, if we
choose to do that.
2019-12-04 00:07:49 -05:00
Greyson Parrelli
608815a69b Add internal pre-alpha support for usernames. 2019-12-04 00:07:42 -05:00
Greyson Parrelli
fb49efa34d Add internal pre-alpha libsignal support for reactions. 2019-12-04 00:07:13 -05:00
Android Team
b20e8616ec Move libsignal-service-java into this repo.
libsignal-service-java repo commit: 1a01c22636
2019-12-04 00:07:13 -05:00
Alan Evans
acf78b6b63 Make new top level gradle file, make app dir and move build.gradle. 2019-12-04 00:07:13 -05:00
Alex Hart
d68fe928b8 Fix nav bar contrast on sticker preview. 2019-12-04 00:07:13 -05:00
Alex Hart
db4d561d9e Fix nav bar color on shared contacts edit screen. 2019-12-04 00:07:07 -05:00
Alex Hart
6933ca50a7 Implement new Message Request UI. 2019-12-04 00:07:07 -05:00
Greyson Parrelli
88c0e6f8ab Fail a job when you can't instantiate it.
This will still crash, but prevent apps from getting into crash loops
when we have bad data in a job.
2019-11-25 11:32:16 -05:00
Greyson Parrelli
6cd4728e3c Update job serialized data after retry. 2019-11-25 11:32:16 -05:00
Greyson Parrelli
97cc82837c Don't backup the JobsDatabase. 2019-11-25 11:32:16 -05:00
Greyson Parrelli
5f0b0e8495 Bump version to 4.50.6 2019-11-25 11:32:07 -05:00
Greyson Parrelli
c837d590ab Removed RefreshUnidentifiedDeliveryAvailabilityJob.
It's been long enough -- it's no longer necessary to check.

Also, the service is going to start returning certs no matter what, so
at this point it's just an unnecessary network call.
2019-11-25 11:32:06 -05:00
Greyson Parrelli
6108a32631 Prevent reading UUIDs from external contacts. 2019-11-25 11:29:58 -05:00
Greyson Parrelli
6d78e1760d Fix possible crash in very old database migration.
The NumberMigrator constructor is crashing. One possibility is that this
migration is running before the user registered, in which case they
wouldn't have a number. Wrapped the parts that don't need to run in this
situation in an `if` block.
2019-11-25 11:29:58 -05:00
Greyson Parrelli
c7b7242eff Fix issues with blocking and MMS groups.
Fixes #9218.

Note that this removes MMS group blocking for now, just because it never
really worked, and I don't want to hotfix in a feature.
2019-11-25 11:29:19 -05:00
Greyson Parrelli
77c687efcf Bump version to 4.50.5 2019-11-21 13:28:23 -05:00
Greyson Parrelli
274af2f010 Ensure group message is being sent to a group. 2019-11-21 13:28:00 -05:00
Greyson Parrelli
6cd5100530 Ensure Recipient.self() is available. 2019-11-21 13:24:54 -05:00
Alan Evans
115a408b0b Bump version to 4.50.4 2019-11-18 09:36:14 -05:00
Alan Evans
d983016137 Updated language translations. 2019-11-18 09:33:53 -05:00
Greyson Parrelli
a8309b6b2b Fix attachment sizes during backup exports. 2019-11-18 09:19:11 -05:00
Alan Evans
e241589c92 Bump version to 4.50.3 2019-11-15 15:43:47 -05:00
Alan Evans
dcb6240a6a Updated language translations. 2019-11-15 15:39:39 -05:00
Alex Hart
599bb5ab0f Update insights copy and queries. 2019-11-15 16:33:54 -04:00
Alan Evans
739fe584c6 Bump version to 4.50.2 2019-11-14 14:49:48 -05:00
Alan Evans
920d1c6207 Updated language translations. 2019-11-14 14:49:48 -05:00
Greyson Parrelli
c1d3d26a15 Removed unnecessary subtext in Insights. 2019-11-14 14:49:48 -05:00
theBoatman
e3b66dc7e8 Fixing RealtimeSleepTimer to work in concurrent threads. 2019-11-14 13:56:12 -05:00
Alan Evans
dfa08d1356 In dark mode, use a dark theme base for RegistrationLockDialog and RationaleDialog.
Fixes #8995
Closes #9077
2019-11-14 13:51:01 -05:00
Curt Brune
6da734c2f9 Update ringrtc to 0.2.0 2019-11-13 14:05:22 -08:00
Alan Evans
a9f3141a77 Create android.yml running qa task. 2019-11-13 15:49:22 -05:00
Alan Evans
ee267d4702 Bump version to 4.50.1 2019-11-13 14:40:04 -05:00
Alan Evans
c39174ed8a Updated language translations. 2019-11-13 14:40:04 -05:00
Greyson Parrelli
9c872b6da6 Fix issue where SMS groups showing up as 'Unnamed Group'. 2019-11-13 11:37:49 -05:00
Greyson Parrelli
2924a09936 Don't run UuidMigration if not registered. 2019-11-13 11:37:49 -05:00
Greyson Parrelli
b9da012cc4 Fix media send issues on Android 4.4.
Before lollipop, Android would ignore activity results for activities
launched with singleTask. So I switched to singleTop, since that should
still solve the problem of double-activity-opens without running into
this issue.

Fixes #9149
2019-11-13 11:37:49 -05:00
Alan Evans
8626d41787 I18N for "Unlock Signal". 2019-11-13 11:37:49 -05:00
Alex Hart
c58705722a Fix Insights Adapter item text clashing. 2019-11-13 11:37:49 -05:00
Greyson Parrelli
c702ff676a Support additional sync behavior for linked devices. 2019-11-13 11:37:49 -05:00
Alan Evans
995569dd50 Bump version to 4.50.0 2019-11-12 10:55:41 -05:00
Greyson Parrelli
a0dfd42527 Fix possible context crash. 2019-11-12 10:55:41 -05:00
Alex Hart
a7dd78cce6 Implement in-app insights. 2019-11-12 10:55:41 -05:00
Greyson Parrelli
e2e9cd40b3 Blacklist additional phone models from the new camera. 2019-11-11 18:13:34 -05:00
Alan Evans
e4fc6f41b8 Schedule first backup after restore for 24hrs time. 2019-11-11 13:00:01 -05:00
Alan Evans
778b2a0d27 Remove old strings file. 2019-11-10 09:23:44 -05:00
Alan Evans
64cd15ca0b Make AlbumThumbnailView_plus non-translatable. 2019-11-10 09:23:43 -05:00
Alan Evans
1e46c3c0ba Make app_name non-translatable. 2019-11-10 09:23:43 -05:00
Alan Evans
91772b4e11 Exclude non-translatable strings.
Lint exclude ExtraTranslation.
2019-11-10 09:23:42 -05:00
Alan Evans
af5c7cb7ca Added dotted lines to time icons. 2019-11-09 07:45:53 -05:00
Alan Evans
e760474fa9 Clear unauthorized flag on connect. 2019-11-09 07:01:08 -05:00
Greyson Parrelli
28bb88e347 Lower max number of JobScheduler IDs.
Some devices can only handle 25 job IDs, so I lowered it to 20.
2019-11-09 07:01:08 -05:00
Curt Brune
17a1fe97ca Send hangup message when terminating on call errors.
When errors occur during a call, attempt to send the remote peer a
hangup message before terminating the call.  This allows the remote
peer to shutdown their side of the call in a timely manner.
2019-11-09 07:01:08 -05:00
Alan Evans
7348237862 Lint.
- baseline update.
- eradicate ButtonOrder.
- eradicate VectorRaster.
- eradicate HardcodedText.
2019-11-09 07:01:08 -05:00
Alex Hart
4e66db5dc1 Fix timer disabled icon. 2019-11-09 07:01:08 -05:00
Alex Hart
ffa6c9acd1 Utilize icon_tint color filter for UpdateItem icons. 2019-11-09 07:01:08 -05:00
Greyson Parrelli
b3e66a9259 Prevent creation of UUID-only recipients. 2019-11-09 07:01:08 -05:00
Alan Evans
5c0cf8c1f0 English staging app name. 2019-11-09 07:01:08 -05:00
Alex Hart
89c716fca4 Fix layout gravity on recv long message footer. 2019-11-09 07:01:08 -05:00
Alan Evans
aae9830dfa Use lib-signal-servivce 2.15.1 2019-11-09 07:01:08 -05:00
Alan Evans
48c72697c1 Add protobuf lite exception for proguard. 2019-11-09 07:01:08 -05:00
Greyson Parrelli
aac7e9ea53 Fix issue with ShareActivity contact display. 2019-11-09 07:01:08 -05:00
Alan Evans
a1b10b3222 Update to protobuf 3.10 lite.
Update fingerprint logic.
Update for additional validation around SignalServiceAddresses.

Co-authored-by: Greyson Parrelli <greyson@signal.org>
2019-11-09 07:01:08 -05:00
Curt Brune
2ab8db33e3 Update ringrtc to 0.1.9 2019-11-09 07:01:08 -05:00
Curt Brune
2b1386232f Implement logging interface for ringrtc-0.1.8
Implement the org.signal.ringrtc.Log.Logger interface, using
org.thoughtcrime.securesms.logging.Log as the underlying logger.

This allows ringrtc to log in the same way as the rest of the
application.
2019-11-09 07:01:07 -05:00
Alex Hart
7bb1caa22e Remove unused pictures that were captured outside of the in-app camera. 2019-11-09 07:01:07 -05:00
Alex Hart
72c14b8651 Apply new Keyboard and Dial Pad icons. 2019-11-09 07:01:07 -05:00
Alex Hart
f85c3bb1e6 Add feature flag to new Profile display. 2019-11-09 07:01:07 -05:00
Alan Evans
d8714fe3b4 Delegate to library Base64. 2019-11-09 07:01:07 -05:00
Alex Hart
c54c1907b6 Fix nav bar colors on reg and call screen. 2019-11-09 07:01:07 -05:00
Greyson Parrelli
55257155dd Handle ProtocolInvalidMessageExceptions with no sender. 2019-11-09 07:01:07 -05:00
alex-signal
55a8bd86de Consolidate profile display labels. 2019-11-09 07:01:07 -05:00
Greyson Parrelli
c60909272b UUID migration. 2019-11-09 07:01:07 -05:00
Greyson Parrelli
3dcc2d8171 Moved DirectoryHelper. 2019-11-09 07:01:07 -05:00
Greyson Parrelli
0ccbb22e4c Unsubscribe from old recipient observers in ConversationActivity. 2019-11-09 07:01:07 -05:00
Greyson Parrelli
0ffa10eaea Bump version to 4.49.18 2019-11-08 20:27:14 -05:00
Alan Evans
9824073023 Keep line numbers within R8. 2019-11-08 16:17:32 -05:00
Alan Evans
d2adc11a2d New languages.
bn,bs,ms,mr,ta,ur
2019-11-08 16:17:32 -05:00
Alan Evans
4d0b870c88 Updated language translations. 2019-11-08 16:17:32 -05:00
Alan Evans
840997cb7d Translate tasks. 2019-11-08 11:41:38 -05:00
Alex Hart
e4406621ed Fix lint issues. 2019-11-08 11:41:38 -05:00
Greyson Parrelli
7f96ee0b62 Clear DATA_HASH column. 2019-11-08 11:30:48 -05:00
Alan Evans
17f850b31a Ensure that we can still navigate when we get a response from server. 2019-11-08 11:19:57 -05:00
Alan Evans
5d87ad0301 Ignore period in phone number entry.
Fixes #9168
2019-11-08 11:19:32 -05:00
Greyson Parrelli
a33eeed6d3 Bump version to 4.49.17 2019-11-07 16:18:27 -05:00
Alex Hart
554ce29337 Adjust Typing Indicator margin in groups. 2019-11-07 16:03:13 -05:00
Alex Hart
6054d0b36f Nullify Data Hash when Data is Nullified 2019-11-07 16:02:56 -05:00
Greyson Parrelli
8ba15cf3b4 Bump version to 4.49.16 2019-11-06 16:42:16 -05:00
Greyson Parrelli
b15e5b4867 Fix potential invalid context crash. 2019-11-06 16:38:28 -05:00
Greyson Parrelli
544511905a Fix issue where deduped media might not be kept in sync. 2019-11-06 16:37:25 -05:00
Greyson Parrelli
02e3f511bd Bump version to 4.49.15 2019-11-04 11:30:08 -05:00
Alex Hart
0539b071e7 Fix message info icon inflation error on API 19 devices.
Fixes #9148
2019-11-04 11:29:31 -05:00
Greyson Parrelli
36e400650c Fix backup restore issue.
Fixes #9133
2019-11-04 11:21:11 -05:00
Greyson Parrelli
d8930daf4b Bump version to 4.49.14 2019-11-01 17:23:52 -04:00
Greyson Parrelli
17b5816d5b Skip corrupt attachments during backup restore.
Instead of aborting the entire backup restore.

Related to #8355
2019-11-01 17:11:10 -04:00
Curt Brune
e574c81ea2 Update ringrtc to 0.1.7.2 2019-11-01 12:26:00 -07:00
Greyson Parrelli
6799a2f841 Bump version to 4.49.13 2019-10-29 16:10:09 -04:00
Curt Brune
61fb20f412 Update ringrtc to v0.1.7 2019-10-29 12:46:01 -07:00
Alan Evans
47f648e7bc Correct text on registration lock fragment. 2019-10-29 13:47:14 -04:00
Alan Evans
064c0ddb82 Manually restrict to 30 digits to allow pasting containing any number of spaces. 2019-10-29 09:59:53 -04:00
Greyson Parrelli
b42c42007d Bump version to 4.49.12 2019-10-28 19:48:07 -04:00
Greyson Parrelli
807cdfce2e Increase backup passphrase input length to 35.
The way pasting works, it could prevent you from pasting in text if you
included the spaces in between number chunks.
2019-10-28 19:47:05 -04:00
Greyson Parrelli
43dc3aeebd Fix 'direct share' icon rendering issue.
Fixes #9138
2019-10-28 16:51:06 -04:00
Greyson Parrelli
0369c5ee16 Make avatars larger in conversations. 2019-10-28 16:46:21 -04:00
Greyson Parrelli
3df2390fe4 Bump version to 4.49.11 2019-10-28 12:28:58 -04:00
Greyson Parrelli
9ec4a7af0f Update max video duration for MMS. 2019-10-28 12:09:54 -04:00
Alex Hart
70636fb4a7 Blacklist Pixel4 from CameraX (#319)
* Blacklist Pixel4 from CameraX

* Create isSupported method
2019-10-28 13:07:28 -03:00
Alex Hart
3c4efdd8f9 Apply proper color when action mode destroyed 2019-10-28 12:00:59 -04:00
Greyson Parrelli
80deb301e5 Bump version to 4.49.10 2019-10-25 14:00:33 -07:00
Greyson Parrelli
5eabfdc34c Fix crash if you leave during log generation. 2019-10-25 14:00:33 -07:00
Greyson Parrelli
5e96832666 Disable video capture for legacy camera devices. 2019-10-25 13:54:38 -07:00
Alan Evans
1ccce24cf8 Fix drawable crash on API 19. 2019-10-25 15:33:34 -04:00
Greyson Parrelli
d59e4f2da7 Bump version to 4.49.9 2019-10-24 14:20:09 -07:00
Greyson Parrelli
a254c1a7b2 Updated language translations. 2019-10-24 14:17:19 -07:00
Alex Hart
82cc610938 Apply themed context to SingleRecipientNotificationBuilder 2019-10-24 17:59:34 -03:00
Greyson Parrelli
becf7c40e8 Bump version to 4.49.8 2019-10-23 23:45:13 -07:00
Greyson Parrelli
ed21c3ca03 Improve send button in media send flow.
Fixes #8988
2019-10-23 23:45:13 -07:00
Greyson Parrelli
631005e565 CameraX cleanup. 2019-10-23 23:45:13 -07:00
Greyson Parrelli
b57fab8c75 Make MediaSendActivity singleTask. 2019-10-23 23:45:13 -07:00
Greyson Parrelli
4260a8436b Fix possible de-duping issues.
- Clean bad hashes from earlier release.
- Fix file equality comparison.
- Given our new de-duping, we don't want to run into a situation where two
simultaneous compressions could be happening on the same image.
2019-10-23 23:45:13 -07:00
Greyson Parrelli
23d478191f Don't render 'null' contact labels. 2019-10-23 23:45:13 -07:00
Greyson Parrelli
d075a33d4e Fix issue where video controls may be missing.
Fixes #9121
2019-10-23 23:45:13 -07:00
Greyson Parrelli
7507dadbe7 Bump version to 4.49.7 2019-10-22 14:17:24 -04:00
Greyson Parrelli
bfd0363390 Fix potential tooltip crash. 2019-10-22 14:17:01 -04:00
Greyson Parrelli
cfb22825f4 Fix recent conversation query. 2019-10-22 13:49:31 -04:00
Greyson Parrelli
d37fafbfe7 Bump version to 4.49.6 2019-10-22 13:02:26 -04:00
Greyson Parrelli
2dc2eb5835 Updated language translations. 2019-10-22 13:02:26 -04:00
Greyson Parrelli
019d036f69 Fix soft nav color in conversation list.
Going in and out of multi-select mode would screw it up.

This fixes it by setting the flags correctly instead of blasting them
all away.
2019-10-22 13:02:26 -04:00
Greyson Parrelli
f1ca5fc8e2 Exclude inactive groups from search results where appropriate.
Fixes #9091
Fixes #9080
2019-10-22 12:41:20 -04:00
Greyson Parrelli
9089fc4001 Fix potential display issues in Message Details screen. 2019-10-22 11:00:03 -04:00
Greyson Parrelli
b281b817ba Add additional logging around attachment upload/deletion. 2019-10-22 10:52:55 -04:00
Greyson Parrelli
cee6736656 Fix flickering 'about' section in group recipient preferences.
Gotta disable those pesky layout animations.

Fixes #9092
2019-10-22 10:30:11 -04:00
Greyson Parrelli
350ca059b9 Pick camera resolution based on screen resolution. 2019-10-22 10:16:14 -04:00
Greyson Parrelli
a7cc5bdc5e Ensure keyboard is hidden when going to the gallery.
Fixes #9120
2019-10-22 09:50:01 -04:00
Greyson Parrelli
2893dfff60 Bump version to 4.49.5 2019-10-21 20:05:31 -04:00
Greyson Parrelli
1c14f06734 Updated language translations. 2019-10-21 20:04:20 -04:00
Greyson Parrelli
ff053437e3 Fix issue where album rail wasn't immediately shown.
Fixes #9111
2019-10-21 20:04:20 -04:00
Alex Hart
03dc220cea Fix crash related to unsupported camera mode.
Fixes #9106
2019-10-21 19:32:19 -04:00
Greyson Parrelli
097f97b5e4 Add system to allow skipping attachment compression. 2019-10-21 19:32:19 -04:00
Alex Hart
95d3db3260 Add ic_message webp files 2019-10-21 15:30:20 -03:00
Greyson Parrelli
0485ae603b Set targetResolution to something reasonable for camera capture. 2019-10-21 10:58:07 -04:00
Alex Hart
2f93da6f1a Fix wrong icon color in expiry footer 2019-10-21 11:50:32 -03:00
Alex Hart
78f3e28974 Fix progress wheel color in share screen 2019-10-21 10:53:18 -03:00
Alex Hart
fecd8300c3 Swap ValueAnimator with version from NineOldAndroids 2019-10-21 10:51:04 -03:00
Alex Hart
262f90dbe7 Add new info icon to message details view 2019-10-21 10:09:54 -03:00
Alex Hart
ed271c6f3e Fix longmessage and shared contact footer alignment 2019-10-21 10:00:28 -03:00
Greyson Parrelli
3fa5843c93 Bump version to 4.49.4 2019-10-21 01:13:19 -04:00
Greyson Parrelli
7f544cb3e5 Updated language translations. 2019-10-21 01:12:51 -04:00
Greyson Parrelli
9f4d40a364 Revert "Prevent compression of images without EXIF data."
This reverts commit 8449d75684.
2019-10-21 01:00:31 -04:00
Greyson Parrelli
09f0c5b63f Fix bug in manual conversation search query submission.
Fixes #9115
2019-10-21 00:49:06 -04:00
Greyson Parrelli
53e0dc5dee Potential MMS download fix for some users.
Special thanks to @mdaniel

Fixes #8571
2019-10-21 00:36:54 -04:00
Greyson Parrelli
8b06051e7c Fix blurhash being visible on transparent images. 2019-10-21 00:21:53 -04:00
Greyson Parrelli
660ec88202 Fix data deduping issues.
Fixes #9109
2019-10-21 00:21:48 -04:00
Greyson Parrelli
b9057a1c11 Use fallback images for Giphy results. 2019-10-20 01:13:02 -04:00
Greyson Parrelli
1f85b1f3d2 Bump version to 4.49.3 2019-10-19 12:37:00 -04:00
Greyson Parrelli
71f78f03f4 Updated language translations. 2019-10-19 12:34:30 -04:00
Greyson Parrelli
4b3d129097 Cleanup some possible bad RecipientIds in the MMS table. 2019-10-19 12:31:06 -04:00
Greyson Parrelli
daeb823399 Fix CameraX crash. 2019-10-19 12:25:40 -04:00
Greyson Parrelli
c7f76c5d1c Add a util to safely set a MediaMetadataRetriever data source. 2019-10-19 12:23:23 -04:00
Greyson Parrelli
9ba1391a1e Fix issue with non-contact avatar colors not updating. 2019-10-19 12:15:14 -04:00
Greyson Parrelli
5d137465e8 Bump version to 4.49.2 2019-10-18 18:46:07 -04:00
Greyson Parrelli
78cbb3c073 Updated language translations. 2019-10-18 18:45:43 -04:00
Greyson Parrelli
097d95428a Fix conversation list toolbar styling. 2019-10-18 17:34:01 -04:00
Greyson Parrelli
2cfb6ede7f Increase record button size. 2019-10-18 17:34:01 -04:00
Greyson Parrelli
695663a5b1 Log more data about video capture support. 2019-10-18 17:34:01 -04:00
Greyson Parrelli
a2204c8370 Update video recording tooltip text. 2019-10-18 17:34:01 -04:00
Alex Hart
8c4757ea02 Fix safety number theme 2019-10-18 17:34:01 -04:00
Greyson Parrelli
85821274fa Don't render null phone number labels. 2019-10-18 17:34:01 -04:00
Greyson Parrelli
a8b1ec8e52 Increase compose maxLines to 5. 2019-10-18 17:34:01 -04:00
Alex Hart
adbdaebd69 Fix invite screen theme. 2019-10-18 17:33:45 -04:00
Alex Hart
c2da4fcd7d Update CameraX to Alpha06 and bring in new View / Module code. 2019-10-18 17:36:00 -03:00
Greyson Parrelli
46ebff3659 Bump version to 4.49.1 2019-10-18 13:58:59 -04:00
Greyson Parrelli
452ccd0350 Updated language translations. 2019-10-18 13:58:35 -04:00
Alan Evans
6e6c809690 Use lighter-weight shadow view. 2019-10-18 13:51:56 -04:00
Greyson Parrelli
7b5c1904cf Update contact list divider styling. 2019-10-18 13:09:20 -04:00
Alex Hart
c0aa9d7587 Update preference divider colors 2019-10-18 13:09:20 -04:00
Alex Hart
5d03e3d516 Apply video recording permissions checks and error handling. 2019-10-18 13:09:20 -04:00
Greyson Parrelli
2bc3a4417f Fix possible crash when retrieving a video thumbnail. 2019-10-18 11:10:26 -04:00
Greyson Parrelli
59d03cbeb2 Address possible issues with AvatarMigrationJob. 2019-10-18 11:08:06 -04:00
Alex Hart
f9f9ae68f5 Fix MediaSendActivity send button metrics and positioning. 2019-10-18 10:49:54 -03:00
Alex Hart
88f2b67984 Fix crash on conversation color picker open. 2019-10-18 10:33:10 -03:00
Greyson Parrelli
44dba4bd98 Only show video tooltip if capable. 2019-10-18 09:31:01 -04:00
Alan Evans
ef585eba42 Revert "Fix invite drawable on API 21."
This reverts commit 78fbfe40736dd8c9ccafb402aa7fc6921c1d4496.

We have turned on AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) now for API19.
2019-10-18 09:15:20 -04:00
Alex Hart
42967dab13 Apply proper tinting to conversation app bar. 2019-10-18 09:55:27 -03:00
Greyson Parrelli
4f75d1a5db Bump version to 4.49.0 2019-10-17 22:45:56 -04:00
Greyson Parrelli
debacbdf58 Updated language translations. 2019-10-17 22:44:32 -04:00
Greyson Parrelli
12c4283c30 Conditionally show a toolbar shadow in the conversation list. 2019-10-17 22:23:27 -04:00
Greyson Parrelli
096c9e0ff7 Ensure videos are paused properly in MediaSendActivity. 2019-10-17 21:33:52 -04:00
Greyson Parrelli
2f23a13a6f Fix issue where video playback controls sometimes don't appear. 2019-10-17 21:33:52 -04:00
Alan Evans
bdadbaaac4 Remove larger images for link preview intro.
(I bet you thought they would be together forever).
2019-10-17 21:33:52 -04:00
Alan Evans
16f41555ba Local SMS rate limiting. 2019-10-17 21:33:52 -04:00
Greyson Parrelli
8c037456e7 Warm up LiveRecipientCache at launch. 2019-10-17 21:33:52 -04:00
Greyson Parrelli
feccee557e Include better defaults for ConversationActivity. 2019-10-17 21:33:52 -04:00
Greyson Parrelli
67f5605bc4 Block in RestStrategy until PushDecryptJobs finish. 2019-10-17 21:33:52 -04:00
Greyson Parrelli
ccb18cd46c Added JobTracker. 2019-10-17 21:33:52 -04:00
Alan Evans
5b1d91016c Fix backup sorting logic and do not delete non-backups in path. 2019-10-17 21:33:52 -04:00
Alan Evans
27ab04ab41 Add version and spread out witness information. 2019-10-17 21:33:52 -04:00
alex-signal
9432a45b39 Implement blur-hash based low resolution thumbnail previews. 2019-10-17 21:33:52 -04:00
alex-signal
9e3475ed94 Upgrade Gradle to 5.6.2 and AGP to 3.5.1 2019-10-17 21:33:52 -04:00
Alan Evans
86d088bce2 Improved registration flow. 2019-10-17 21:33:52 -04:00
alex-signal
a135e7efa2 Check DND settings before show activity or play ring or vibrate. 2019-10-17 21:33:52 -04:00
tzm
c247935f1a Fix NPE when has no contact permissions.
Fixes #9048

Co-authored-by: Alan Evans <alan@signal.org>
2019-10-17 21:33:52 -04:00
Alan Evans
31657f5c15 Gradle witness sort dependencies and move to own auto generated file. 2019-10-17 21:33:52 -04:00
Greyson Parrelli
bcecc30d33 Disallow RecipientId's of zero. 2019-10-17 21:33:52 -04:00
Greyson Parrelli
7193252d77 Fix TooltipPopup positioning. 2019-10-17 21:33:51 -04:00
Greyson Parrelli
5b682a3a3d Use our own location retriever. 2019-10-17 21:33:51 -04:00
Greyson Parrelli
6b8659a393 Move JobManager to ApplicationDependencies. 2019-10-17 21:33:51 -04:00
Alan Evans
14557d3dc1 Change default GZIP behavior on Base64.decode(String).
Remove only reference to Base64.decode(byte[]).
2019-10-17 21:33:51 -04:00
Curt Brune
03cbee0277 Add ringrtc support.
RingRTC provides Signal Messenger applications with a common interface
for video and voice calling services built on top of WebRTC.
2019-10-17 21:33:51 -04:00
alex-signal
adc0907906 Copy image to encrypted blob and delete from device immediately.
Fixes #9097
2019-10-17 21:33:51 -04:00
Alan Evans
f14a71a076 Update Lint suppressions. 2019-10-17 21:33:51 -04:00
alex-signal
12c2b53f7c Set icon to themed version via attribute.
Fixes #9054
2019-10-17 21:33:51 -04:00
alex-signal
ff60b5b731 Add in-app video recording for supported devices. 2019-10-17 21:33:51 -04:00
alex-signal
43954a176a Apply new Signal icons and color palette. 2019-10-17 21:33:51 -04:00
Alex Hart
d698d3bd6f Added support for view-once video. 2019-10-17 16:01:34 -04:00
Greyson Parrelli
50a81c0e60 Added a simple staging build config. 2019-10-17 16:01:34 -04:00
Greyson Parrelli
b0b8377a8e Migrate notification channels to recipientId's. 2019-10-17 16:01:34 -04:00
alex-signal
7d02bb8487 Store file hash to avoid data duplication. 2019-10-17 16:01:34 -04:00
alex-signal
8449d75684 Prevent compression of images without EXIF data. 2019-10-17 16:01:34 -04:00
Greyson Parrelli
b89163bb14 Migrate avatars to use recipientId filenames. 2019-10-17 16:01:34 -04:00
Greyson Parrelli
b7ce220600 Updated libsignal version to 2.13.8 2019-10-15 10:35:59 -04:00
Greyson Parrelli
948079a42e Bump version to 4.48.17 2019-10-15 10:35:15 -04:00
Greyson Parrelli
de1c6cdd0c Fix crash when MMS messages have no 'from' address. 2019-10-15 10:35:15 -04:00
Greyson Parrelli
37147fb0a5 Bump version to 4.48.16 2019-10-08 21:28:54 -07:00
Greyson Parrelli
33334f80c3 Add back proper support for unknown recipients.
Fixes #9085
2019-10-08 21:28:35 -07:00
Greyson Parrelli
36286da9bd Bump version to 4.48.15 2019-10-08 14:23:29 -07:00
Greyson Parrelli
2248acb9f2 Remove unnecessary resolves. 2019-10-08 14:23:29 -07:00
Greyson Parrelli
3632a2cc95 Hide no-name contacts from system search results. 2019-10-08 13:38:20 -07:00
Greyson Parrelli
9447ea12cb Remove concept of 'unknown' recipient. 2019-10-08 13:33:13 -07:00
Greyson Parrelli
89c2329fdf Bump version to 4.48.14 2019-10-07 16:52:36 -07:00
Greyson Parrelli
84012c7adb Fix crash in SmsMigrator. 2019-10-07 16:41:47 -07:00
Greyson Parrelli
d7395af774 Simplify recipient inserts. 2019-10-07 16:31:04 -07:00
Greyson Parrelli
be4daff0f3 Ensure recipient models are up-to-date. 2019-10-07 16:31:04 -07:00
Greyson Parrelli
c2459d0a31 Bump version to 4.48.13 2019-10-04 12:13:16 -04:00
Greyson Parrelli
5f993ed0f7 Fix threading issue in RecipientDatabase. 2019-10-04 12:07:46 -04:00
Greyson Parrelli
acea24c19c Reduce Recipient refreshes. 2019-10-04 10:52:43 -04:00
Greyson Parrelli
82b5d58d04 Fix threading issues in LiveRecipient. 2019-10-04 10:52:43 -04:00
Greyson Parrelli
af2990fa08 Bump version to 4.48.12 2019-10-03 16:08:18 -04:00
Greyson Parrelli
0fa48540e1 Fix migration of old pending SMS sends. 2019-10-03 16:07:53 -04:00
Greyson Parrelli
0f06b96832 Bump version to 4.48.11 2019-10-03 11:18:51 -04:00
Greyson Parrelli
4f423be6f9 Updated language translations. 2019-10-03 11:18:07 -04:00
Greyson Parrelli
bea21ed5ff Further recipient insertion improvements. 2019-10-03 11:17:48 -04:00
Greyson Parrelli
a9cff032f5 Bump version to 4.48.10 2019-10-02 20:44:22 -04:00
Greyson Parrelli
b2d02d4ace Add extra protections to recipient insertions. 2019-10-02 20:13:49 -04:00
Greyson Parrelli
066df77abf Revert "Use recipientId's in ContactSelectionListAdapter."
This reverts commit 89e075c56e.
2019-10-02 19:26:24 -04:00
Greyson Parrelli
b38a3e6259 Remove unnecessary recipient resolves. 2019-10-02 19:16:59 -04:00
Greyson Parrelli
d78919acf8 Bump version to 4.48.9 2019-10-02 12:36:24 -04:00
Greyson Parrelli
af96d11188 Pretty-print missing recipientId crashes. 2019-10-02 12:22:26 -04:00
Greyson Parrelli
5e1bef26ed Fix display of profile names in FTS results. 2019-10-02 12:22:26 -04:00
Greyson Parrelli
95333eccd4 Cleanup bad recipients. 2019-10-02 12:22:26 -04:00
Greyson Parrelli
89e075c56e Use recipientId's in ContactSelectionListAdapter. 2019-10-02 11:24:55 -04:00
Greyson Parrelli
d026498a8c Use recipientId's in SearchRepository. 2019-10-02 11:24:55 -04:00
Greyson Parrelli
bae381e6f8 Bump version to 4.48.8 2019-10-01 08:20:16 -04:00
Greyson Parrelli
050f7a24c9 Updated language translations. 2019-10-01 08:19:46 -04:00
Greyson Parrelli
77ad1ea729 Prevent ConversationActivity recipientId crash. 2019-10-01 08:17:25 -04:00
Greyson Parrelli
bf667c0cfc Revert "Add logging to track down ConversationActivity crash."
This reverts commit 447236ee38.
2019-10-01 08:16:57 -04:00
Greyson Parrelli
ccb8ef98b4 Bump version to 4.48.7 2019-09-30 15:19:41 -04:00
Greyson Parrelli
a1f0660339 Updated language translations. 2019-09-30 15:19:24 -04:00
Greyson Parrelli
447236ee38 Add logging to track down ConversationActivity crash. 2019-09-30 15:19:24 -04:00
Alan Evans
ebf3b0dfe1 Change versionCode for universal builds. 2019-09-30 14:16:49 -04:00
Greyson Parrelli
ee9216df8a Ensure group status is available in ConversationAdapter constructor. 2019-09-30 14:15:19 -04:00
Greyson Parrelli
3e34668232 Bump version to 4.48.6 2019-09-28 00:16:41 -04:00
Curt Brune
8f8f41f184 Localize call audio and video activation code 2019-09-28 00:16:09 -04:00
Greyson Parrelli
4f7cba8d7c Bump version to 4.48.5 2019-09-27 14:26:08 -04:00
Greyson Parrelli
00b719c783 Updated language translations. 2019-09-27 14:22:13 -04:00
Greyson Parrelli
b03eccec33 Persist the JobFactory key after job migrations. 2019-09-27 14:18:15 -04:00
Greyson Parrelli
5805539deb Revert "Potential MMS download fix for some users."
This reverts commit 1e375ec494.
2019-09-27 14:02:01 -04:00
Greyson Parrelli
1b48fd07a3 Bump version to 4.48.4 2019-09-26 18:32:28 -04:00
Greyson Parrelli
1e375ec494 Potential MMS download fix for some users.
Special thanks to @mdaniel

Fixes #8571
2019-09-26 18:32:28 -04:00
Greyson Parrelli
ee9acf2687 Show tooltip for swipe-to-reply. 2019-09-26 16:37:09 -04:00
Greyson Parrelli
1d8e85fcad Updated the feel of swipe-to-reply. 2019-09-26 16:36:52 -04:00
Greyson Parrelli
4e2afa7362 Fix some more broken JobMigrations. 2019-09-26 11:18:04 -04:00
Greyson Parrelli
d06564c7b9 Bump version to 4.48.3 2019-09-25 19:12:24 -04:00
Greyson Parrelli
ced48d0788 Fix custom notification creation. 2019-09-25 18:58:21 -04:00
Greyson Parrelli
e273593343 Fix more possible JobMigration crashes. 2019-09-25 15:19:08 -04:00
Greyson Parrelli
c5767b07a7 Initialize CameraX on a background thread.
We saw a situation where CameraX initialization could lock up when
trying to obtain system resource. We already fallback to the
Camera1Fragment when CameraX isn't initialized, so it should be safe to
do this initialization on its own thread.
2019-09-25 15:17:35 -04:00
Greyson Parrelli
709ce7e9de Bump version to 4.48.2 2019-09-25 11:37:55 -04:00
Greyson Parrelli
a8f7908ed4 Updated language translations. 2019-09-25 11:37:55 -04:00
Greyson Parrelli
8230786638 Added permissions to the debug log. 2019-09-25 11:23:23 -04:00
Greyson Parrelli
df4ecc4e32 Add blocked threads to the debug log. 2019-09-25 11:23:23 -04:00
Greyson Parrelli
5b755b9501 Crash early when using a null RecipientId. 2019-09-25 09:51:50 -04:00
Greyson Parrelli
b3c7c8db5c Fix possible crash in DirectoryRefreshJob. 2019-09-25 09:51:50 -04:00
Greyson Parrelli
a21c537428 Fix crash during JobMigration. 2019-09-25 09:51:50 -04:00
Greyson Parrelli
6d339cd023 Fix a crash when starting a call from the system contacts. 2019-09-25 09:51:50 -04:00
Alan Evans
840c493265 Fix pretty print phone numbers.
Fixes #9047
2019-09-25 09:49:26 -04:00
alex-signal
14999800e2 Fix Swipe to Reply progress reset when AnimatorDurationScale is 0.
Fixes #9051
2019-09-25 10:08:10 -03:00
Greyson Parrelli
8e332d0798 Bump version to 4.48.1 2019-09-24 12:13:27 -04:00
Greyson Parrelli
cd007a20d7 Fix possible thread visibility issue in CellConstraintObserver.
Related to #9030.
2019-09-24 12:13:27 -04:00
Greyson Parrelli
8b99af3eef Fix threading issues with LiveRecipient. 2019-09-24 12:03:33 -04:00
Greyson Parrelli
d354de806e Bump version to 4.48.0 2019-09-24 10:24:37 -04:00
alex-signal
c7ef0c06f8 Fix exif dimension edge case and handle invalid dimens better in ThumbnailView. 2019-09-24 10:11:17 -04:00
Greyson Parrelli
9d14bcb808 Enable sticker sharing. 2019-09-24 10:11:17 -04:00
alex-signal
de03706d8d Fix video thumbnail not displaying after sharing to conversation thread. 2019-09-24 10:11:17 -04:00
alex-signal
a3caabcafd Fix video forwarding thumbnail display. 2019-09-24 10:11:17 -04:00
alex-signal
faafa40122 Fix DecryptableStreamLocalUriFetcher bug for external video thumbnails.
Fix bug where external video thumbnails do not appear.
2019-09-24 10:11:17 -04:00
Greyson Parrelli
d1a6582ad7 Support independent application migration versions. 2019-09-24 10:11:17 -04:00
alex-signal
f81c0b448e Add CameraXFlashToggleView and selfie flash. 2019-09-24 10:11:17 -04:00
Greyson Parrelli
70347e754c Add a feature flag system. 2019-09-24 10:11:17 -04:00
Greyson Parrelli
a1245baf61 Don't show MMS groups in recent Signal contacts list. 2019-09-24 10:11:17 -04:00
Alex Hart
007ea43dc8 Add Swipe-To-Reply in conversations.
Swipe-To-Reply is a progress based animation that is controlled
by the ConversationItemSwipeCallback.  We use the TouchListener to
keep track of the latest down coordinates, so that we can check whether
we are over a seek bar. The SwipeAnimationHelper is responsible for
actually performing the transitions as the swipe progresses.
2019-09-24 10:11:17 -04:00
Greyson Parrelli
582028f2c2 Search contacts via the RecipientDatabase. 2019-09-24 10:11:17 -04:00
Greyson Parrelli
0e2d52026e Migrated to locally-assigned RecipientId's.
Oh boy.
2019-09-24 10:11:17 -04:00
Greyson Parrelli
233cc7ecce Bump version to 4.47.6 2019-09-16 17:00:06 -04:00
Greyson Parrelli
1baf03f51a Fix message fetching bug when app is in the foreground.
Fixes #9036
2019-09-16 16:58:51 -04:00
Greyson Parrelli
f066a9cab2 Bump version to 4.47.5 2019-09-11 18:01:50 -04:00
Curt Brune
825d1a02ab Update to WebRTC M77 2019-09-11 13:55:26 -07:00
Curt Brune
97cc8b7777 Revert "Add ringrtc support."
Revert the following commits:

"Handle busy call while in PSTN call."
Commit 23a0bb3ce0.

"Add ringrtc support."
Commit 3ac540c687.
2019-09-11 11:17:23 -07:00
Greyson Parrelli
72662b5b52 Bump version to 4.47.4 2019-09-06 13:56:17 -04:00
Greyson Parrelli
ea4e0b6d6f Updated language translations. 2019-09-06 13:56:04 -04:00
Alan Evans
6df93c0bb5 Fix potential contact search crash. 2019-09-06 13:53:26 -04:00
Curt Brune
23a0bb3ce0 Handle busy call while in PSTN call. 2019-09-06 10:20:32 -07:00
Greyson Parrelli
4cd5256267 Bump version to 4.47.3 2019-09-04 11:10:47 -04:00
Greyson Parrelli
5362e07a5c Fix crash that may happen upon first sticker install. 2019-09-04 11:09:30 -04:00
Greyson Parrelli
936bd327bd Bump version to 4.47.2 2019-09-03 15:38:34 -04:00
Greyson Parrelli
db89619a4a Updated language translations. 2019-09-03 15:38:20 -04:00
Alan Evans
b5ce7325fc Fix invite drawable on API 19. 2019-09-03 15:20:09 -04:00
Greyson Parrelli
ccaeb089ab Bump version to 4.47.1 2019-09-02 18:57:14 -04:00
Greyson Parrelli
3240ba3a55 Updated language translations. 2019-09-02 18:53:57 -04:00
Alan Evans
bb7be66efe Fix stickers in image editor.
Fixes #9012
2019-09-02 18:16:30 -04:00
Alan Evans
8814a0d949 Update invite icon. 2019-09-02 18:15:27 -04:00
Greyson Parrelli
9aa488223f Bump version to 4.47.0 2019-08-31 11:28:34 -04:00
Greyson Parrelli
30d9233365 Updated language translations. 2019-08-31 11:28:34 -04:00
Curt Brune
3ac540c687 Add ringrtc support.
RingRTC provides Signal Messenger applications with a common interface
for video and voice calling services built on top of WebRTC.
2019-08-31 07:54:47 -07:00
Alan Evans
8d561ead21 Stickers in image editor. 2019-08-30 17:45:35 -04:00
Alan Evans
15b650382e Fullscreen media preview on tap. 2019-08-30 17:31:59 -04:00
Oscar Mira
110a40592b Make E164 log scrubber redact numbers of length >= 7. 2019-08-30 08:20:21 -04:00
Alan Evans
d0ce4ff032 Lottie play pause animation. 2019-08-29 11:57:41 -04:00
Alan Evans
85c9a9050a Add an invite button in the new conversation screen. 2019-08-29 11:07:33 -04:00
Greyson Parrelli
af42d5b671 Create system for job migrations. 2019-08-29 11:07:33 -04:00
Greyson Parrelli
9580bb0a38 Renamed WorkManager migration package. 2019-08-29 11:07:33 -04:00
Greyson Parrelli
9abb167874 Bump version to 4.46.2 2019-08-28 10:27:40 -04:00
Greyson Parrelli
cd13676a21 Updated language translations. 2019-08-28 10:26:29 -04:00
Greyson Parrelli
1dd59bee36 Use raw versionCodes in web apk json.
We missed a spot when transitioning to split apk versions. It ended up
breaking the update prompt for web apks.

This will put the raw universal apk version in the json file, which
should put us back on the right track.

Fixes #8936
2019-08-28 10:26:29 -04:00
Greyson Parrelli
59bcbe592b Revert "Add ringrtc support"
This reverts commit 7f0a7b0c13.
2019-08-28 09:16:51 -04:00
Greyson Parrelli
0917e17c69 Fix bug where you couldn't reply with media.
Fixes ##8999
2019-08-27 09:40:32 -04:00
Greyson Parrelli
eb249ca69a Bump version to 4.46.1 2019-08-24 15:18:22 -04:00
Greyson Parrelli
97d1175915 Updated language translations. 2019-08-24 14:35:35 -04:00
Alan Evans
2141f1073e Ensure memory file descriptor is available. 2019-08-24 14:30:02 -04:00
Greyson Parrelli
c6287547a3 Improve video transcoding exception handling.
Previously, we'd crash if we couldn't find the attachment at runtime,
but that's not uncommon if someone deletes before sending. Now we just
fail.

Also, previously we'd fail if we couldn't create a memory file. Now we
will only fail if the attachment is >100mb (same as the other video
failures).
2019-08-24 09:27:08 -04:00
Greyson Parrelli
9257c6ddf3 Fix failure propogation in longer job chains. 2019-08-24 09:09:20 -04:00
Greyson Parrelli
480748e1aa Bump version to 4.46.0 2019-08-23 10:01:29 -04:00
Greyson Parrelli
c015687951 Updated language translations. 2019-08-23 10:01:06 -04:00
Alan Evans
1bd1e9cc65 Read optional video display dimensions.
Add RequiresApi annotations.

Fixes #8982
2019-08-22 10:15:56 -04:00
Alan Evans
c4a4374465 RTL screen animations.
RTL fix Profile name in Settings.
2019-08-22 10:15:56 -04:00
Alan Evans
90681d47f8 Fix first item sticky header. 2019-08-22 10:15:56 -04:00
Alan Evans
936be693ce Enlarge some home touch targets and add search content description. 2019-08-22 10:15:56 -04:00
Alan Evans
a32b875587 Support disabled animations for accessibility. 2019-08-22 10:15:56 -04:00
Alan Evans
4e6798e38e Fix occasional double scroll bar on fast scroller. 2019-08-22 10:15:56 -04:00
Alan Evans
6352f7baf4 Update item design in contact selection. 2019-08-22 10:15:56 -04:00
Alan Evans
aac9725adc Move shared attachment job generation code to parent class. 2019-08-22 10:15:56 -04:00
Alan Evans
900371bb30 Call answer button listens to accessibility changes. 2019-08-22 10:15:56 -04:00
Alan Evans
a58f564d1e Escape string within Full Text Search.
Fixes #8975
2019-08-22 10:15:56 -04:00
Alan Evans
942154a61f Separate compression job. 2019-08-22 10:04:23 -04:00
Curt Brune
7f0a7b0c13 Add ringrtc support
Initial commit of the RingRTC Java interface implementation.

The implementation lives in an external .aar with the package
org.signal.ringrtc.

The package provides two high level objects of interest
=======================================================

org.signal.ringrtc.CallConnection -- represents the session of a call,
very similar to WebRTC's PeerConnection.

org.signal.ringrtc.CallConnectionFactory -- creates CallConnection
objects, very similar to WebRTC's PeerConnectionFactory.

The implementation interfaces with the Android application in a few
places:
==================================================================

src/org/thoughtcrime/securesms/ApplicationContext.java -- RingRTC
library initialization at application startup.

src/org/thoughtcrime/securesms/service/WebRtcCallService.java -- Call
creation and state machine.

src/org/thoughtcrime/securesms/ringrtc -- this package implements
interface classes needed by ringrtc and a CallConnectionWrapper helper
class.

The two interfaces needed so far are:

  ringrtc/Logger.java
  ringrtc/SignalMessageRecipient.java

The logger is self-explanatory, but SignalMessageRecipient is a little
more involved.  SignalMessageRecipient encapsulates the Signal-Android
notion of "Recipient" and the mechanism for sending Signal Messages
related to audio/video calling.

The CallConnectionWrapper class is clone of the original
org.thoughtcrime.securesms.webrtc.PeerConnectionWrapper, suitably
modified to match the CallConnection interface.

This class continues to handle the Camera switching APIs, with that
portion of the code remaining unmodified from the original.

CallConnectionFactory Details
=============================

The primary public methods:

initialize() -- initialize the WebRTC library and RingRTC library.
The WebRTC initialization is lifted from the original Signal-Android
code.

createCallConnectionFactory() -- creates a CallConnectionFactory
object.  Internally it creates a WebRTC PeerConnectionFactory object
and a RingRTC CallConnectionFactory object.

dispose() -- tears down the CallConnectionFactory object, including
the internal PeerConnectionFactory and RingRTC CallConnectionFactory.

createCallConnection() -- creates a CallConnection object, connecting
that with an application controlled CallConnection.Observer object.

This function takes a CallConnection.Configuration object to link the
CallConnection object with some application provided services, like
sending Signal protocol messages.

CallConnection Details
======================

This object is a subclass of WebRTC's PeerConnection class.

The primary public methods and objects:

CallConnection.Configuration
----------------------------

Configuration object used to parameterize a call.  Notable members:

- SignalServiceMessageSender messageSender
- long callId
- org.signal.SignalMessageRecipient recipient

The 'accountManager' is used to fetch public information from the Signal
service, specifically used here to obtain the public Signal TURN
server details.

The 'callId' is a 64-bit pseudo-random number generated when the call
is initiated, used to identify the call through out its lifetime.

The "recipient' is an implementation of the
org.signal.SignalMessageRecipient interface, which encapsulates the
sending of Signal service messages to a recipient (remote peer) using
existing Signal protocol data structures.

The native library needs to be able to send Signal messages via the
service, but it does not have a native implementation to do so.
Instead the native code calls out to the client for sending Signal
messages.  To accomplish this, the client implements the
org.signal.SignalMessageRecipient interface and passes an instance of
that in a CallConnection.Configuration object.

CallConnection
--------------

dispose() -- tears down the CallConnection object, including the
internal PeerConnection and RingRTC CallConnection.

sendOffer() -- initiates a call to a remote recipient.  This is the
beginning of an outbound call.

validateResponse() -- checks an offer response recipient against the
originating call details.

handleOfferAnswer() -- handles the receipt of answer, which was a
response from an originating offer.

acceptOffer() -- accept an offer from a remote participant.  This is
the begin of an incoming call.

answerCall() -- invoked when the call is completely established and
online.

hangUp() -- hang up the connection and shut things done.  This is the
end of the call.

sendBusy() -- send the remote side an indication that the local side
is already in a call and the line is busy.

sendVideoStatus() -- send the current state of the local camera video
stream to the remote side.

CallConnection.Observer
-----------------------

Observer object, used by the RingRTC library to notify the client
application of important events and status changes.  Similar in spirit
to WebRTC's PeerConnection.Observer.

Observer callbacks come in three flavors:

- state change notifications,
- on stream notifications
- errors conditions

For state notifications, the callback contains the callId, the
recipient and a CallConnection.CallEvent type.

For streams, the callback contains the callId, the
recipient and a org.webrtc.MediaStream.

For errors, the callback contains the callId, the recipient and an
exception type.  The currently thrown exceptions include:

- UntrustedIdentityException
- UnregisteredUserException
- IOException

Signed-off-by: Curt Brune <curt@signal.org>

Updates to support ringrtc-android version 0.1.0.

* simplify logging interface

It is no longer necessary for the application to specify a Log object
as the library can log via the NDK directly.

* improve error handling and notification

In a number of places where ringrtc errors could occur, no
notification was ever sent to the user, nor was the UI cleaned up.  It
would look like the app was in hung state.

This patch updates these situations to send the WebRtcViewModel a
NETWORK_FAILURE message.

* update handleIncomingCall() for lockManager and notification

During the conversion to RingRTC, the implementation of
handleIncomingCall() missed a couple of things:

-- updating the Phone state with the lockManager
-- sending a message to the viewModel

* log the callId in various handler methods

For debugging purposes it is very handy to have the callId present in
the log during the various call handler methods.

Signed-off-by: Curt Brune <curt@signal.org>
2019-08-22 10:04:23 -04:00
Alan Evans
37bcac40bb URL encoded scrubber.
* Replace scrubber and tests.

* Improves email regex performance.
2019-08-22 10:04:23 -04:00
Greyson Parrelli
02ea99254a Reset message receiver upon REST failure.
We've been seeing a lot of socket timeouts on REST requests under
certain conditions. The issue seems to be a problem with the OkHttp
client. Through testing, I've seen that resetting the receiver and
retrying again seems to resolve most issues.
2019-08-22 10:04:23 -04:00
Greyson Parrelli
3849b46f0a Refactor ApplicationDependencies. 2019-08-22 10:04:23 -04:00
Greyson Parrelli
116bd41c63 Use EmojiTextView in the sticker preview. 2019-08-22 10:04:23 -04:00
Greyson Parrelli
457ad4c607 Added a central system for message retrieval. 2019-08-22 10:04:23 -04:00
Greyson Parrelli
d0a9bd4c6d Create a new system for application-level migrations. 2019-08-22 10:04:23 -04:00
Greyson Parrelli
d3bed549f2 Switch back to storing incoming messages in PushDatabase.
On a Pixel 3, this adds ~30-40ms of delay, but it's going to be a
requirement for upcoming migrations.
2019-08-22 10:04:23 -04:00
Alan Evans
fe1aa016b9 Refactor group operations. 2019-08-22 10:04:17 -04:00
1562 changed files with 81280 additions and 31611 deletions

17
.github/workflows/android.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Android CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew qa

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.classpath
captures/
project.properties
.project
.settings

View File

@@ -27,6 +27,7 @@
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
@@ -113,22 +114,20 @@
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
android:launchMode="singleTask"/>
<activity android:name=".CountrySelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".InviteActivity"
android:theme="@style/TextSecure.HighlightTheme"
android:theme="@style/Signal.Light.NoActionBar.Invite"
android:windowSoftInputMode="stateHidden"
android:parentActivityName=".ConversationListActivity"
android:parentActivityName=".MainActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".PromptMmsActivity"
@@ -191,16 +190,8 @@
</intent-filter>
</activity>
<activity android:name=".ConversationListActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true" />
<activity-alias android:name=".RoutingActivity"
android:targetActivity=".ConversationListActivity"
android:targetActivity=".MainActivity"
android:exported="true">
<intent-filter>
@@ -216,24 +207,14 @@
</activity-alias>
<activity android:name=".ConversationListArchiveActivity"
android:label="@string/AndroidManifest_archived_conversations"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".ConversationListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
</activity>
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".ConversationListActivity">
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".longmessage.LongMessageActivity" />
@@ -261,7 +242,7 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseUpgradeActivity"
<activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -298,10 +279,11 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.MediaSendActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.MediaSendActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseChangeActivity"
android:label="@string/AndroidManifest__change_passphrase"
@@ -318,24 +300,12 @@
</intent-filter>
</activity>
<activity android:name=".registration.WelcomeActivity"
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".RegistrationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".registration.CaptchaActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"
@@ -364,10 +334,9 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaOverviewActivity"
<activity android:name=".mediaoverview.MediaOverviewActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DummyActivity"
@@ -429,6 +398,10 @@
android:theme="@style/TextSecure.DarkTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".scribbles.NewStickerSelectActivity"
android:theme="@style/TextSecure.DarkTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/TextSecure.DarkTheme"/>
@@ -469,7 +442,18 @@
<activity
android:name=".maps.PlacePickerActivity"
android:label="@string/PlacePickerActivity_title"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity
android:name=".usernames.ProfileEditActivityV2"
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="adjustResize"/>
<activity android:name=".MainActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>

462
app/build.gradle Normal file
View File

@@ -0,0 +1,462 @@
import org.signal.signing.ApkSignerUtil
import java.security.MessageDigest
buildscript {
repositories {
google()
maven {
url "https://repo1.maven.org/maven2"
}
jcenter {
content {
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
}
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'witness'
apply from: 'translations.gradle'
apply from: 'witness-verifications.gradle'
repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
content {
includeGroupByRegex "com\\.github\\.chrisbanes.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/shortcutbadger/releases/"
content {
includeGroupByRegex "me\\.leolin.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
content {
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content {
includeGroupByRegex "org\\.signal.*"
}
}
maven { // textdrawable
url 'https://dl.bintray.com/amulyakhare/maven'
content {
includeGroupByRegex "com\\.amulyakhare.*"
}
}
google()
mavenCentral()
jcenter()
mavenLocal()
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.10.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.navigation:navigation-fragment:2.1.0'
implementation 'androidx.navigation:navigation-ui:2.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
implementation "androidx.camera:camera-core:1.0.0-alpha06"
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
implementation('com.google.firebase:firebase-messaging:17.3.4') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
implementation 'com.google.android.gms:play-services-maps:16.1.0'
implementation 'com.google.android.gms:play-services-auth:16.0.1'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation project(':libsignal-service')
implementation 'org.signal:ringrtc-android:0.2.0'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0'
implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
exclude group: 'com.squareup.okhttp', module: 'okhttp'
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.airbnb.android:lottie:3.0.7'
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.freemarker'
}
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:1.9.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.multidex:multidex:2.0.1'
androidTestImplementation 'androidx.multidex:multidex-instrumentation:2.0.0'
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestImplementation ('org.assertj:assertj-core:1.7.1') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
androidTestImplementation ('com.squareup.assertj:assertj-android:1.1.1') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
exclude group: 'com.android.support', module: 'support-annotations'
}
testImplementation 'org.robolectric:robolectric:4.2'
testImplementation 'org.robolectric:shadows-multidex:4.2'
}
dependencyVerification {
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
}
def canonicalVersionCode = 579
def canonicalVersionName = "4.51.6"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
'armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4]
android {
flavorDimensions "none"
compileSdkVersion 28
buildToolsVersion '28.0.3'
useLibrary 'org.apache.http.legacy'
dexOptions {
javaMaxHeapSize "4g"
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion 19
targetSdkVersion 28
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "Signal");
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
buildConfigField "boolean", "DEV_BUILD", "false"
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"f2e2a5004794a6c1bac5c4949eadbc243dd02e02d1a93f10fe24584fb70815d8\""
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"f51f435802ada769e67aaf5744372bb7e7d519eecf996d335eb5b46b872b5789\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
resConfigs autoResConfig()
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
}
buildTypes {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard-firebase-messaging.pro',
'proguard/proguard-google-play-services.pro',
'proguard/proguard-jackson.pro',
'proguard/proguard-sqlite.pro',
'proguard/proguard-appcompat-v7.pro',
'proguard/proguard-square-okhttp.pro',
'proguard/proguard-square-okio.pro',
'proguard/proguard-spongycastle.pro',
'proguard/proguard-rounded-image-view.pro',
'proguard/proguard-glide.pro',
'proguard/proguard-shortcutbadger.pro',
'proguard/proguard-retrofit.pro',
'proguard/proguard-webrtc.pro',
'proguard/proguard-klinker.pro',
'proguard/proguard-retrolambda.pro',
'proguard/proguard-okhttp.pro',
'proguard/proguard-ez-vcard.pro',
'proguard/proguard.cfg'
testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg'
}
staging {
initWith debug
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"b5a865941f95887018c86725cc92308d34a3084dc2b4e7bd2de5e5e1690b50c6\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
}
release {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
}
}
productFlavors {
play {
dimension "none"
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
website {
dimension "none"
ext.websiteUpdateUrl = "https://updates.signal.org/android"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
}
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
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']
proto.srcDir '../protobuf'
}
androidTest {
java.srcDirs = ['../test/androidTest/java']
}
test {
java.srcDirs = ['../test/unitTest/java']
resources.srcDirs = ['../test/unitTest/resources']
}
staging {
res.srcDirs = ['../staging/res']
}
website.manifest.srcFile '../website/AndroidManifest.xml'
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
disable "LintError"
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
def assembleWebsiteDescriptor = { variant, file ->
if (file.exists()) {
MessageDigest md = MessageDigest.getInstance("SHA-256");
file.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size);
}
String digest = md.digest().collect {String.format "%02x", it}.join();
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl
String apkName = file.getName()
String descriptor = "{" +
"\"versionCode\" : ${canonicalVersionCode * postFixSize + abiPostFix['universal']}," +
"\"versionName\" : \"$canonicalVersionName\"," +
"\"sha256sum\" : \"$digest\"," +
"\"url\" : \"$url/$apkName\"" +
"}"
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
descriptorFile.write(descriptor)
}
}
def signProductionRelease = { variant ->
variant.outputs.collect { output ->
String apkName = output.outputFile.name
File inputFile = new File(output.outputFile.path)
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
'pkcs11.config',
'PKCS11',
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
outputFile.getAbsolutePath())
inputFile.delete()
outputFile
}
}
task signProductionPlayRelease {
doLast {
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
}
}
task signProductionWebsiteRelease {
doLast {
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
assembleWebsiteDescriptor(variant, signedRelease)
}
}
tasks.whenTaskAdded { task ->
if (task.name.equals("assemblePlayRelease")) {
task.finalizedBy signProductionPlayRelease
}
if (task.name.equals("assembleWebsiteRelease")) {
task.finalizedBy signProductionWebsiteRelease
}
}
def getLastCommitTimestamp() {
new ByteArrayOutputStream().withStream { os ->
def result = exec {
executable = 'git'
args = ['log', '-1', '--pretty=format:%ct']
standardOutput = os
}
return os.toString() + "000"
}
}

View File

@@ -1,13 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="5" by="lint 3.3.2" client="gradle" variant="playRelease" version="3.3.2">
<issue
id="LintError"
message="Unexpected failure during lint analysis of JobManager.java (this is a bug in lint or one of the libraries it depends on)&#xA;&#xA;Stack: `NullPointerException:ClsFileImpl.getMirror(ClsFileImpl.java:343)←ClsElementImpl.getMirror(ClsElementImpl.java:159)←ClsElementImpl.getText(ClsElementImpl.java:202)←InferenceSession.argConstraints(InferenceSession.java:1817)←InferenceSession.isFunctionalTypeMoreSpecific(InferenceSession.java:1748)←InferenceSession.isFunctionalTypeMoreSpecificOnExpression(InferenceSession.java:1729)←JavaMethodsConflictResolver.isFunctionalTypeMoreSpecific(JavaMethodsConflictResolver.java:779)←JavaMethodsConflictResolver.isTypeMoreSpecific(JavaMethodsConflictResolver.java:673)`&#xA;&#xA;You can set environment variable `LINT_PRINT_STACKTRACE=true` to dump a full stacktrace to stdout.">
<location
file="src/org/thoughtcrime/securesms/jobmanager/JobManager.java"/>
</issue>
<issue
id="MissingPermission"
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"

View File

@@ -12,5 +12,14 @@
<issue id="ImpliedQuantity" severity="warning" />
<issue id="CanvasSize" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="VectorRaster" severity="error" />
<issue id="ButtonOrder" severity="error" />
<issue id="ExtraTranslation" severity="warning" />
<issue id="RestrictedApi" severity="error">
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java" />
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java" />
</issue>
</lint>

View File

@@ -1,4 +1,5 @@
-dontoptimize
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; }
-keep class org.thoughtcrime.securesms.** { *; }
@@ -6,3 +7,5 @@
public void onEvent*(**);
}
# Protobuf lite
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }

108
app/translations.gradle Normal file
View File

@@ -0,0 +1,108 @@
import groovy.io.FileType
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
ext {
autoResConfig = this.&autoResConfig
}
def allStringsResourceFiles(@ClosureParams(value = SimpleType.class, options = ['java.io.File']) Closure c) {
file('../res').eachFileRecurse(FileType.FILES) { f ->
if (f.name == 'strings.xml') {
c(f)
}
}
}
/**
* Discovers supported languages listed as under the res/values- directory.
*/
def autoResConfig() {
def files = []
allStringsResourceFiles { f ->
files.add(f.parentFile.name)
}
['en'] + files.collect { f -> f =~ /^values-([a-z]{2}(-r[A-Z]{2})?)$/ }
.findAll { matcher -> matcher.find() }
.collect { matcher -> matcher.group(1) }
.sort()
}
task pullTranslations(type: Exec) {
group 'Translate'
description 'Pull translations, requires transifex client and api key.'
commandLine 'tx', 'pull', '-a', '--minimum-perc=80', '--force'
}
task replaceEllipsis {
group 'Translate'
description 'Process strings for ellipsis characters.'
doLast {
allStringsResourceFiles { f ->
def before = f.text
def after = f.text.replace('...', '…')
if (before != after) {
f.text = after
logger.info("$f.parentFile.name/$f.name...updated")
}
}
}
mustRunAfter pullTranslations
}
task cleanApostropheErrors {
group 'Translate'
description 'Fix transifex apostrophe string errors.'
doLast {
allStringsResourceFiles { f ->
def before = f.text
def after = before.replaceAll(/([^\\=08])(')/, '$1\\\\\'')
if (before != after) {
f.text = after
logger.info("$f.parentFile.name/$f.name...updated")
}
}
}
mustRunAfter replaceEllipsis
}
task excludeNonTranslatables {
group 'Translate'
description 'Remove strings that are marked "translatable"="false" or are ExtraTranslations.'
doLast {
def englishFile = file('../res/values/strings.xml')
def english = new XmlParser().parse(englishFile)
def nonTranslatable = english
.findAll { it['@translatable'] == 'false' }
.collect { it['@name'] }
.toSet()
def all = english.collect { it['@name'] }.toSet()
def translatable = all - nonTranslatable
allStringsResourceFiles { f ->
if (f != englishFile) {
def newLines = f.readLines()
.collect { line ->
def matcher = line =~ /name="([^"]*)".*<\//
if (matcher.find()) {
def name = matcher.group(1)
if (!line.contains('excludeNonTranslatables') && !translatable.contains(name)) {
return " <!-- Removed by excludeNonTranslatables ${line.trim()} -->"
}
}
return line
}
f.write(newLines.join("\n") + "\n")
}
}
}
mustRunAfter cleanApostropheErrors
}
task translate {
group 'Translate'
description 'Pull translations and post-process for ellipsis, apostrophes and non-translatables.'
dependsOn pullTranslations, replaceEllipsis, cleanApostropheErrors, excludeNonTranslatables
}

View File

@@ -0,0 +1,375 @@
// Auto-generated, use ./gradlew calculateChecksums to regenerate
dependencyVerification {
verify = [
['androidx.activity:activity:1.0.0',
'd1bc9842455c2e534415d88c44df4d52413b478db9093a1ba36324f705f44c3d'],
['androidx.annotation:annotation:1.1.0',
'd38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692'],
['androidx.appcompat:appcompat-resources:1.1.0-beta01',
'53c0a33d07c4bab48d4c8169bf30053aa14965af4a775b56092a9fc7079802b1'],
['androidx.appcompat:appcompat:1.1.0-beta01',
'49ad229add44f822fcb3c8405c3fddbd72660da6a839ce29e13158f5980514fd'],
['androidx.arch.core:core-common:2.1.0',
'fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889'],
['androidx.arch.core:core-runtime:2.1.0',
'dd77615bd3dd275afb11b62df25bae46b10b4a117cd37943af45bdcbf8755852'],
['androidx.asynclayoutinflater:asynclayoutinflater:1.0.0',
'f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b'],
['androidx.camera:camera-camera2:1.0.0-alpha06',
'e50f20deb950ffebcd4d1de5408ef7a5404bec80ec77119e05663c890739b903'],
['androidx.camera:camera-core:1.0.0-alpha06',
'0096cabe539d9b4288f406acfb44264b137ebd600e38e33504ff425c979016c9'],
['androidx.cardview:cardview:1.0.0',
'1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],
['androidx.collection:collection:1.1.0',
'632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72'],
['androidx.concurrent:concurrent-futures:1.0.0-alpha03',
'50812a53912255e3e0f2147d13bbbb81937c3726fda2e984e77a27c7207d96a1'],
['androidx.constraintlayout:constraintlayout-solver:1.1.3',
'965c177e64fbd81bd1d27b402b66ef9d7bc7b5cb5f718044bf7a453abc542045'],
['androidx.constraintlayout:constraintlayout:1.1.3',
'5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830'],
['androidx.coordinatorlayout:coordinatorlayout:1.0.0',
'e508c695489493374d942bf7b4ee02abf7571d25aac4c622e57d6cd5cd29eb73'],
['androidx.core:core:1.1.0',
'76c7cfbe596fe3c09a6983bf1c89e889299c08ac9a3b52ce5182a088d056647e'],
['androidx.cursoradapter:cursoradapter:1.0.0',
'a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564'],
['androidx.customview:customview:1.0.0',
'20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2'],
['androidx.documentfile:documentfile:1.0.0',
'865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487'],
['androidx.drawerlayout:drawerlayout:1.0.0',
'9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1'],
['androidx.exifinterface:exifinterface:1.0.0',
'ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11'],
['androidx.fragment:fragment:1.1.0',
'a14c8b8f2153f128e800fbd266a6beab1c283982a29ec570d2cc05d307d81496'],
['androidx.gridlayout:gridlayout:1.0.0',
'a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755'],
['androidx.interpolator:interpolator:1.0.0',
'33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a'],
['androidx.legacy:legacy-preference-v14:1.0.0',
'd6d11913e56b8f2d14fd560bd1ad6d7fd5624a15dd4ec073b2d9188205f86280'],
['androidx.legacy:legacy-support-core-ui:1.0.0',
'0d1260c6e7e6a337f875df71b516931e703f716e90889817cd3a20fa5ac3d947'],
['androidx.legacy:legacy-support-core-utils:1.0.0',
'a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7'],
['androidx.legacy:legacy-support-v13:1.0.0',
'65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86'],
['androidx.legacy:legacy-support-v4:1.0.0',
'78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0'],
['androidx.lifecycle:lifecycle-common-java8:2.1.0',
'a1ec63c1bb973443cb731d78ec336c5e20e7ee35c89cbb32d36f92c55bb02542'],
['androidx.lifecycle:lifecycle-common:2.2.0-alpha05',
'63898dabf7cfe5ec5d7ed8b8c2564c1427be876e1496ead95c2703cf59d3734b'],
['androidx.lifecycle:lifecycle-extensions:2.1.0',
'bd53c64b038585215b4959c1a388437a3ad525608a31c58e4283c3e371727d4d'],
['androidx.lifecycle:lifecycle-livedata-core:2.2.0-alpha05',
'6df2bcbf3be50a5fa29e9aa09d39437f2d61d17c2c46ef618d65bbac4a4a99fc'],
['androidx.lifecycle:lifecycle-livedata:2.1.0',
'242e446bed3db36f0df0aab0cb7f91060bd2dab7adcad1117adf54e724cd1d26'],
['androidx.lifecycle:lifecycle-process:2.1.0',
'8cddd0c7f4927bbf71fb71fca000786df82cc597c99463d6916ccbe4a205a9ac'],
['androidx.lifecycle:lifecycle-runtime:2.1.0',
'e5173897b965e870651e83d9d5af1742d3f532d58863223a390ce3a194c8312b'],
['androidx.lifecycle:lifecycle-service:2.1.0',
'23516745f34f16ff7850bb1eadd55cf193dd789cba428de4bca120433e3bfd69'],
['androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05',
'f503b53f50c4e6c1f9a3d698c4733df6e7a44049fe477ad0b85cc2f460401fbc'],
['androidx.lifecycle:lifecycle-viewmodel:2.2.0-alpha05',
'7725715491963440ee483e46526cd4f83af1c758e072e97b3eab2115c6f4db35'],
['androidx.loader:loader:1.0.0',
'11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025'],
['androidx.localbroadcastmanager:localbroadcastmanager:1.0.0',
'e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8'],
['androidx.media:media:1.0.0',
'b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee'],
['androidx.multidex:multidex:2.0.1',
'42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09'],
['androidx.navigation:navigation-common:2.1.0',
'f968fcaa2fd94b0d1275ce175ecfb4773678732ead9b4d81993ffd5bc3fe3c7c'],
['androidx.navigation:navigation-fragment:2.1.0',
'776ba1be826f8de7cb262f55ece262c5eb9947758cbd2e902298750521404dd2'],
['androidx.navigation:navigation-runtime:2.1.0',
'499029c016345a2a2130ee7a32670871757e5fc7e6d1b93be8962bb59fa5ce9d'],
['androidx.navigation:navigation-ui:2.1.0',
'1ec0558d692982c5bcfcca6de5b5972723e6b4a9870aa7fc1eddf5e869f116ed'],
['androidx.preference:preference:1.0.0',
'ea9fde25606eb456210ffe9f7e51048abd776b55a34c0cc6608282b5699122d1'],
['androidx.print:print:1.0.0',
'1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd'],
['androidx.recyclerview:recyclerview:1.0.0',
'06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548'],
['androidx.savedstate:savedstate:1.0.0',
'2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83'],
['androidx.slidingpanelayout:slidingpanelayout:1.0.0',
'76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede'],
['androidx.swiperefreshlayout:swiperefreshlayout:1.0.0',
'9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d'],
['androidx.transition:transition:1.0.1',
'c374bef04f01580ba76447e759ea560079727779ff882ad55735fd445edca8b4'],
['androidx.vectordrawable:vectordrawable-animated:1.1.0-beta02',
'f1613c47f1e6d2cd02ec9a42925f1a964fa63d1d028d34d884364cc3b9ffcb8f'],
['androidx.vectordrawable:vectordrawable:1.1.0-beta02',
'b632152304edb506bf7eacb329ef41e43b80164bf5be4c7bb132a249a65cbc26'],
['androidx.versionedparcelable:versionedparcelable:1.1.0',
'9a1d77140ac222b7866b5054ee7d159bc1800987ed2d46dd6afdd145abb710c1'],
['androidx.viewpager:viewpager:1.0.0',
'147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682'],
['cn.carbswang.android:NumberPickerView:1.0.9',
'18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729'],
['com.airbnb.android:lottie:3.0.7',
'6819ff968eb768096133c7873d63351705fd4ac424a0917d86c4145f5035097d'],
['com.amulyakhare:com.amulyakhare.textdrawable:1.0.1',
'54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb'],
['com.annimon:stream:1.1.8',
'5da6e2e3e0551d61a3ea7014f04312276549e3dd739cf637996e4cf43c5535b9'],
['com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4',
'5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794'],
['com.davemorrissey.labs:subsampling-scale-image-view:3.6.0',
'550c5baa07e0bb4ff0a18b705e96d34436d22619248bd8c08c08c730b1f55cfe'],
['com.fasterxml.jackson.core:jackson-annotations:2.9.0',
'45d32ac61ef8a744b464c54c2b3414be571016dd46bfc2bec226761cf7ae457a'],
['com.fasterxml.jackson.core:jackson-core:2.9.9',
'3083079be6088db2ed0a0c6ff92204e0aa48fa1de9db5b59c468f35acf882c2c'],
['com.fasterxml.jackson.core:jackson-databind:2.9.9.2',
'fb262d42ea2de98044b62d393950a5aa050435fec38bbcadf2325cf7dc41b848'],
['com.github.bumptech.glide:annotations:4.9.0',
'702a7521cb3f6d7e55edd66e90bda1a1975baf971d25f75b75638579f86bc69b'],
['com.github.bumptech.glide:disklrucache:4.9.0',
'4696a81340eb6beee21ab93f703ed6e7ae49fb4ce3bc2fbc546e5bacd21b96b9'],
['com.github.bumptech.glide:gifdecoder:4.9.0',
'7ee9402ae1c48fac9232b67e81f881c217b907b3252e49ce57bdb97937ebb270'],
['com.github.bumptech.glide:glide:4.9.0',
'1bf482442fce81aa9065a5e97e721039d921cc45f727a987be5f1a69f844d955'],
['com.github.chrisbanes:PhotoView:2.1.3',
'ed06775308da260e1fd86d1d3288988fcd3d80db24ce0d7c9fcfedc39e622292'],
['com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2',
'8dc6a29a5a8db7b2ad5a9a7fda1dc9ae0893f4c8f0545732b2c63854ea693e8e'],
['com.google.android.exoplayer:exoplayer-core:2.9.1',
'b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0'],
['com.google.android.exoplayer:exoplayer-ui:2.9.1',
'7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151'],
['com.google.android.gms:play-services-auth-api-phone:16.0.0',
'19365818b9ceb048ef48db12b5ffadd5eb86dbeb2c7c7b823bfdd89c665f42e5'],
['com.google.android.gms:play-services-auth-base:16.0.0',
'51dc02ad2f8d1d9dff7b5b52c4df2c6c12ef7df55d752e919d5cb4dd6002ecd0'],
['com.google.android.gms:play-services-auth:16.0.1',
'aec9e1c584d442cb9f59481a50b2c66dc191872607c04d97ecb82dd0eb5149ec'],
['com.google.android.gms:play-services-base:16.0.1',
'aca10c780c3219bc50f3db06734f4ab88badd3113c564c0a3156ff8ff674655b'],
['com.google.android.gms:play-services-basement:16.0.1',
'e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6'],
['com.google.android.gms:play-services-maps:16.1.0',
'ff50cae9e4059416202375597d99cdc8ddefd9cea3f1dc2ff53779a3a12eb480'],
['com.google.android.gms:play-services-stats:16.0.1',
'5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368'],
['com.google.android.gms:play-services-tasks:16.0.1',
'b31c18d8d1cc8d9814f295ee7435471333f370ba5bd904ca14f8f2bec4f35c35'],
['com.google.android.material:material:1.0.0',
'7680e381a3c03798d999b2e441caadd8a56a0a808e108024a67af9fe26c11adc'],
['com.google.android:flexbox:0.3.0',
'a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935'],
['com.google.auto.value:auto-value-annotations:1.6.3',
'0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0'],
['com.google.firebase:firebase-common:16.0.3',
'3db6bfd4c6f758551e5f9acdeada2050577277e6da1aefb2412de23829759bcf'],
['com.google.firebase:firebase-iid-interop:16.0.1',
'2a86322b9346fd4836219206d249e85803311655e96036a8e4b714ce7e79693b'],
['com.google.firebase:firebase-iid:17.0.4',
'bb42774e309d5eac1aa493d19711032bee4f677a409639b6a5cfa93089af93eb'],
['com.google.firebase:firebase-messaging:17.3.4',
'e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078'],
['com.google.guava:listenablefuture:1.0',
'e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069'],
['com.google.protobuf:protobuf-javalite:3.10.0',
'215a94dbe100130295906b531bb72a26965c7ac8fcd9a75bf8054a8ac2abf4b4'],
['com.google.zxing:android-integration:3.1.0',
'89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4'],
['com.google.zxing:core:3.2.1',
'b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259'],
['com.googlecode.ez-vcard:ez-vcard:0.9.11',
'7e24ad50b222d2f70ac91bdccfa3c0f6200b078d797cb784837f75e77bb4210f'],
['com.googlecode.libphonenumber:libphonenumber:8.11.0',
'833ec343e55a6910daca392af76f8e09caaab9c4e3f41ded83f4746e7ef389c7'],
['com.jpardogo.materialtabstrip:library:1.0.9',
'c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa'],
['com.klinkerapps:android-smsmms:4.0.1',
'e7c3328a0f3a8dd44daa8129de4e99996f3057a4546e47891b036b81e0ebf1d1'],
['com.klinkerapps:logger:1.0.3',
'177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541'],
['com.makeramen:roundedimageview:2.1.0',
'1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1'],
['com.melnykov:floatingactionbutton:1.3.0',
'15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263'],
['com.nineoldandroids:library:2.4.0',
'68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a'],
['com.pnikosis:materialish-progress:1.5',
'd71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54'],
['com.squareup.okhttp3:okhttp:3.12.1',
'07c3d82ca7eaf4722f00b2da807dc7860f6169ae60cfedcf5d40218f90880a46'],
['com.squareup.okio:okio:1.17.4',
'd78fac588458fc099e6c82e91fe5f0375c67434626451a3a77772c65d9eee85b'],
['com.takisoft.fix:colorpicker:0.9.1',
'f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1'],
['com.theartofdev.edmodo:android-image-cropper:2.8.0',
'5516ea87672e478b3d0ed5c274a7df27d22c02e66f899388f9b8bee93669e176'],
['com.tomergoldst.android:tooltips:1.0.6',
'4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6'],
['me.leolin:ShortcutBadger:1.1.16',
'e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774'],
['mobi.upod:time-duration-picker:1.1.3',
'db469ce0f48dd96b892eac424ed76870e54bf00fe0a28cdcddfbe5f2a226a0e1'],
['org.apache.httpcomponents:httpclient-android:4.3.5',
'6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1'],
['org.conscrypt:conscrypt-android:2.0.0',
'400ca559a49b860a82862b22cee0e3110764bdcf7ee7c79e7479895c25cdfc09'],
['org.greenrobot:eventbus:3.0.0',
'180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c'],
['org.jsoup:jsoup:1.8.3',
'abeaf34795a4de70f72aed6de5966d2955ec7eb348eeb813324f23c999575473'],
['org.signal:aesgcmprovider:0.0.3',
'6eb4422e8a618b3b76cb2096a3619d251f9e27989dc68307a1e5414c3710f2d1'],
['org.signal:android-database-sqlcipher:3.5.9-S3',
'33d4063336893af00b9d68b418e7b290cace74c20ce8aacffddc0911010d3d73'],
['org.signal:ringrtc-android:0.2.0',
'bce32dfe6c4fe107ccd4b9f155dcfaefe574e7740ef57190614b7e34c914f5f0'],
['org.signal:signal-metadata-java:0.1.0',
'f3faa23b7d9b5096d12979c35679d1e3b5e007522d8bef167a28e456f2a7c7d9'],
['org.threeten:threetenbp:1.3.6',
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
['org.whispersystems:curve25519-java:0.5.0',
'0aadd43cf01d11e9b58f867b3c4f25c3194e8b0623d1953d32dfbfbee009e38d'],
['org.whispersystems:signal-protocol-java:2.8.1',
'b19db36839ab008fdccefc7f8c005f2ea43dc7c7298a209bc424e6f9b6d5617b'],
['pl.tajchert:waitingdots:0.1.0',
'2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'],
['se.emilsjolander:stickylistheaders:2.7.0',
'a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb'],
]
}

View File

@@ -1,554 +0,0 @@
import org.signal.signing.ApkSignerUtil
import java.security.MessageDigest
buildscript {
repositories {
google()
maven {
url "https://repo1.maven.org/maven2"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
}
}
apply plugin: 'com.android.application'
apply plugin: 'witness'
repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
content {
includeGroupByRegex "com\\.github\\.chrisbanes.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/shortcutbadger/releases/"
content {
includeGroupByRegex "me\\.leolin.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
content {
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content {
includeGroupByRegex "org\\.signal.*"
}
}
maven { // textdrawable
url 'https://dl.bintray.com/amulyakhare/maven'
content {
includeGroupByRegex "com\\.amulyakhare.*"
}
}
google()
jcenter()
mavenLocal()
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.0.0'
implementation "androidx.camera:camera-core:1.0.0-alpha02"
implementation "androidx.camera:camera-camera2:1.0.0-alpha02"
implementation('com.google.firebase:firebase-messaging:17.3.4') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
implementation 'com.google.android.gms:play-services-maps:16.1.0'
implementation 'com.google.android.gms:play-services-location:16.0.0'
implementation 'com.google.android.gms:play-services-auth:16.0.1'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation 'org.whispersystems:signal-service-android:2.13.7'
implementation 'org.whispersystems:webrtc-android:M75'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0'
implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
exclude group: 'com.squareup.okhttp', module: 'okhttp'
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.freemarker'
}
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:1.9.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.multidex:multidex:2.0.1'
androidTestImplementation 'androidx.multidex:multidex-instrumentation:2.0.0'
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestImplementation ('org.assertj:assertj-core:1.7.1') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
}
androidTestImplementation ('com.squareup.assertj:assertj-android:1.1.1') {
exclude group: 'org.hamcrest', module: 'hamcrest-core'
exclude group: 'com.android.support', module: 'support-annotations'
}
testImplementation 'org.robolectric:robolectric:4.2'
testImplementation 'org.robolectric:shadows-multidex:4.2'
}
dependencyVerification {
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
verify = [
'com.google.android.material:material:7680e381a3c03798d999b2e441caadd8a56a0a808e108024a67af9fe26c11adc',
'androidx.legacy:legacy-preference-v14:d6d11913e56b8f2d14fd560bd1ad6d7fd5624a15dd4ec073b2d9188205f86280',
'androidx.preference:preference:ea9fde25606eb456210ffe9f7e51048abd776b55a34c0cc6608282b5699122d1',
'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54',
'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c',
'com.theartofdev.edmodo:android-image-cropper:5516ea87672e478b3d0ed5c274a7df27d22c02e66f899388f9b8bee93669e176',
'mobi.upod:time-duration-picker:db469ce0f48dd96b892eac424ed76870e54bf00fe0a28cdcddfbe5f2a226a0e1',
'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729',
'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6',
'com.takisoft.fix:colorpicker:f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1',
'com.codewaves.stickyheadergrid:stickyheadergrid:5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794',
'androidx.appcompat:appcompat:49ad229add44f822fcb3c8405c3fddbd72660da6a839ce29e13158f5980514fd',
'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
'androidx.recyclerview:recyclerview:06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548',
'androidx.legacy:legacy-support-v13:65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86',
'androidx.cardview:cardview:1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7',
'androidx.gridlayout:gridlayout:a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755',
'androidx.camera:camera-camera2:9dc33e45da983ebd29a888401ac700323ff573821eee3fa4d993dfa3d316ee2e',
'androidx.camera:camera-core:bf32bfcb5d103d865c6af1221a1d82e994c917b53c0bc080f1e9750bdc21cbb9',
'androidx.exifinterface:exifinterface:ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11',
'androidx.constraintlayout:constraintlayout:5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830',
'androidx.multidex:multidex:42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09',
'androidx.lifecycle:lifecycle-extensions:8d4072201b6231d67e4192d608d46b1f5c920845106c9831632c2e3ffe706117',
'androidx.lifecycle:lifecycle-common-java8:9edc2d4f589656d470ef03b9c6ece62d335971294b033ec7d9ceb6e361e9aafa',
'com.google.firebase:firebase-messaging:e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078',
'com.google.android.gms:play-services-maps:ff50cae9e4059416202375597d99cdc8ddefd9cea3f1dc2ff53779a3a12eb480',
'com.google.android.gms:play-services-location:240a0fcb9e8e58586e38ea43b69c09ed6e89ea9a0c69770b7634d81dabf5f3a0',
'com.google.android.gms:play-services-auth:aec9e1c584d442cb9f59481a50b2c66dc191872607c04d97ecb82dd0eb5149ec',
'com.google.android.exoplayer:exoplayer-ui:7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151',
'com.google.android.exoplayer:exoplayer-core:b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0',
'org.conscrypt:conscrypt-android:400ca559a49b860a82862b22cee0e3110764bdcf7ee7c79e7479895c25cdfc09',
'org.signal:aesgcmprovider:6eb4422e8a618b3b76cb2096a3619d251f9e27989dc68307a1e5414c3710f2d1',
'org.whispersystems:signal-service-android:5115aa434c52ca671c513995e6ae67d73f3abaaa605f9e6cf64c2e01da961c7e',
'org.whispersystems:webrtc-android:f8231bb57923afb243760213dc58924e85cce42f2f3cc8cb33a6d883672a921a',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa',
'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
'com.github.chrisbanes:PhotoView:ed06775308da260e1fd86d1d3288988fcd3d80db24ce0d7c9fcfedc39e622292',
'com.github.bumptech.glide:glide:1bf482442fce81aa9065a5e97e721039d921cc45f727a987be5f1a69f844d955',
'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1',
'org.greenrobot:eventbus:180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c',
'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.davemorrissey.labs:subsampling-scale-image-view:550c5baa07e0bb4ff0a18b705e96d34436d22619248bd8c08c08c730b1f55cfe',
'com.klinkerapps:android-smsmms:e7c3328a0f3a8dd44daa8129de4e99996f3057a4546e47891b036b81e0ebf1d1',
'com.annimon:stream:5da6e2e3e0551d61a3ea7014f04312276549e3dd739cf637996e4cf43c5535b9',
'com.github.dmytrodanylyk.circular-progress-button:library:8dc6a29a5a8db7b2ad5a9a7fda1dc9ae0893f4c8f0545732b2c63854ea693e8e',
'org.signal:android-database-sqlcipher:33d4063336893af00b9d68b418e7b290cace74c20ce8aacffddc0911010d3d73',
'com.googlecode.ez-vcard:ez-vcard:7e24ad50b222d2f70ac91bdccfa3c0f6200b078d797cb784837f75e77bb4210f',
'com.google.firebase:firebase-iid:bb42774e309d5eac1aa493d19711032bee4f677a409639b6a5cfa93089af93eb',
'com.google.firebase:firebase-common:3db6bfd4c6f758551e5f9acdeada2050577277e6da1aefb2412de23829759bcf',
'com.google.android.gms:play-services-auth-api-phone:19365818b9ceb048ef48db12b5ffadd5eb86dbeb2c7c7b823bfdd89c665f42e5',
'com.google.android.gms:play-services-auth-base:51dc02ad2f8d1d9dff7b5b52c4df2c6c12ef7df55d752e919d5cb4dd6002ecd0',
'com.google.firebase:firebase-iid-interop:2a86322b9346fd4836219206d249e85803311655e96036a8e4b714ce7e79693b',
'com.google.android.gms:play-services-base:aca10c780c3219bc50f3db06734f4ab88badd3113c564c0a3156ff8ff674655b',
'com.google.android.gms:play-services-tasks:b31c18d8d1cc8d9814f295ee7435471333f370ba5bd904ca14f8f2bec4f35c35',
'com.google.android.gms:play-services-places-placereport:04f8baeb1f8f8a734c7d4b1701a3974281b45591affa7e963b59dd019b8abc6e',
'com.google.android.gms:play-services-stats:5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368',
'com.google.android.gms:play-services-basement:e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6',
'androidx.legacy:legacy-support-v4:78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0',
'androidx.fragment:fragment:9656d81c472b5142bbc3471ef7259fbc93905dc38e823c63a99e48819881b6e7',
'androidx.appcompat:appcompat-resources:53c0a33d07c4bab48d4c8169bf30053aa14965af4a775b56092a9fc7079802b1',
'androidx.legacy:legacy-support-core-ui:0d1260c6e7e6a337f875df71b516931e703f716e90889817cd3a20fa5ac3d947',
'androidx.drawerlayout:drawerlayout:9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1',
'androidx.legacy:legacy-support-core-utils:a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7',
'androidx.transition:transition:a00a0f763f401abcecda9b0eafcb738929c5801b111a9a414b81a193d0f4008d',
'androidx.media:media:b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee',
'androidx.viewpager:viewpager:147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682',
'androidx.loader:loader:11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025',
'androidx.activity:activity:0d6bafb56a72da893f3990ca5d819214d047f5f6b5c5f822ed97971c05eeb85a',
'androidx.vectordrawable:vectordrawable-animated:f1613c47f1e6d2cd02ec9a42925f1a964fa63d1d028d34d884364cc3b9ffcb8f',
'androidx.vectordrawable:vectordrawable:b632152304edb506bf7eacb329ef41e43b80164bf5be4c7bb132a249a65cbc26',
'androidx.coordinatorlayout:coordinatorlayout:e508c695489493374d942bf7b4ee02abf7571d25aac4c622e57d6cd5cd29eb73',
'androidx.slidingpanelayout:slidingpanelayout:76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede',
'androidx.customview:customview:20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2',
'androidx.swiperefreshlayout:swiperefreshlayout:9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d',
'androidx.asynclayoutinflater:asynclayoutinflater:f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b',
'androidx.core:core:45c7a50ad1f366e62db496d8cef7730d5ee1681215007d1a19e6b6d800a12842',
'androidx.cursoradapter:cursoradapter:a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564',
'androidx.lifecycle:lifecycle-livedata:c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39',
'androidx.lifecycle:lifecycle-livedata-core:fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc',
'androidx.arch.core:core-runtime:87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e',
'androidx.concurrent:concurrent-listenablefuture-callback:14dce0acbffd705cfe9fb378960f851a9d8fc3f293d1157c310c9624a561d0a8',
'androidx.concurrent:concurrent-listenablefuture:f9ef396ca4a43b9685d28bec117b278aa9171de0e446e5138e931074e3462feb',
'com.github.bumptech.glide:gifdecoder:7ee9402ae1c48fac9232b67e81f881c217b907b3252e49ce57bdb97937ebb270',
'androidx.versionedparcelable:versionedparcelable:948c751f6352d4c0f93f15fa1bf506c59083bc7754264dd9a325a6da0e2eec05',
'androidx.collection:collection:632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72',
'androidx.interpolator:interpolator:33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a',
'androidx.documentfile:documentfile:865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487',
'androidx.localbroadcastmanager:localbroadcastmanager:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8',
'androidx.print:print:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd',
'androidx.lifecycle:lifecycle-process:d8ff6fd844559743050c9ae010a6df230f2a3dbdf3e14498316f30bd8df836b5',
'androidx.lifecycle:lifecycle-service:cb2b15bb0cf14134e953ed8ead96f94265018643f519367d51fd837f7311e9f8',
'androidx.lifecycle:lifecycle-runtime:7e6d414d03bb184f3015dacc6233eeaded45fa23f0cf4c1f6d3395d6495fa41c',
'androidx.lifecycle:lifecycle-viewmodel:9f2efb59328027fa9f0c413d4d5910aab68d149b139ca8ce432135105b74833a',
'androidx.savedstate:savedstate:115ac7313095b2d159565d2bc851a7722e43fc00347fc828214ff8917799b5f0',
'androidx.lifecycle:lifecycle-common:76db6be533bd730fb361c2feb12a2c26d9952824746847da82601ef81f082643',
'androidx.arch.core:core-common:fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889',
'androidx.annotation:annotation:d38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692',
'androidx.constraintlayout:constraintlayout-solver:965c177e64fbd81bd1d27b402b66ef9d7bc7b5cb5f718044bf7a453abc542045',
'com.google.auto.value:auto-value-annotations:0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0',
'org.signal:signal-metadata-android:02323bc29317fa9d3b62fab0b507c94ba2e9bcc4a78d588888ffd313853757b3',
'org.whispersystems:signal-service-java:34c1efbfdc9cca44946a92f1ba330066bc533056a4db3359a1af96e519893b2e',
'com.github.bumptech.glide:disklrucache:4696a81340eb6beee21ab93f703ed6e7ae49fb4ce3bc2fbc546e5bacd21b96b9',
'com.github.bumptech.glide:annotations:702a7521cb3f6d7e55edd66e90bda1a1975baf971d25f75b75638579f86bc69b',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'com.klinkerapps:logger:177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541',
'com.google.android:flexbox:a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935',
'org.jsoup:jsoup:abeaf34795a4de70f72aed6de5966d2955ec7eb348eeb813324f23c999575473',
'org.whispersystems:signal-protocol-android:c80aac5f93114da2810e2e89437831f79fcbc8bece652f64aeab313a651cba85',
'org.signal:signal-metadata-java:2ce71cc4ec5dacfbaef4a265fceef61b8a09696b541994106a22a946762cbdcc',
'org.whispersystems:signal-protocol-java:7f6df67a963acbab7716424b01b12fa7279f18a9623a2a7c8ba7b1c285830168',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'com.googlecode.libphonenumber:libphonenumber:dbf4bf566d17a60044c19e282a619684e4b4abb0f9f9f24f843c55d19826ab5e',
'com.fasterxml.jackson.core:jackson-databind:fb262d42ea2de98044b62d393950a5aa050435fec38bbcadf2325cf7dc41b848',
'com.squareup.okhttp3:okhttp:07c3d82ca7eaf4722f00b2da807dc7860f6169ae60cfedcf5d40218f90880a46',
'org.threeten:threetenbp:f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7',
'org.whispersystems:curve25519-android:b502bcf83efe001f09a7a9efda6f0fa772c43ed5924e97816296ed3503caa092',
'com.fasterxml.jackson.core:jackson-annotations:45d32ac61ef8a744b464c54c2b3414be571016dd46bfc2bec226761cf7ae457a',
'com.fasterxml.jackson.core:jackson-core:3083079be6088db2ed0a0c6ff92204e0aa48fa1de9db5b59c468f35acf882c2c',
'com.squareup.okio:okio:693fa319a7e8843300602b204023b7674f106ebcb577f2dd5807212b66118bd2',
'org.whispersystems:curve25519-java:0aadd43cf01d11e9b58f867b3c4f25c3194e8b0623d1953d32dfbfbee009e38d',
]
}
def canonicalVersionCode = 518
def canonicalVersionName = "4.45.2"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4,
'universal' : 5]
android {
flavorDimensions "none"
compileSdkVersion 28
buildToolsVersion '28.0.3'
useLibrary 'org.apache.http.legacy'
dexOptions {
javaMaxHeapSize "4g"
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion 19
targetSdkVersion 28
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "Signal");
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
buildConfigField "boolean", "DEV_BUILD", "false"
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
resConfigs autoResConfig()
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
}
buildTypes {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-firebase-messaging.pro',
'proguard-google-play-services.pro',
'proguard-jackson.pro',
'proguard-sqlite.pro',
'proguard-appcompat-v7.pro',
'proguard-square-okhttp.pro',
'proguard-square-okio.pro',
'proguard-spongycastle.pro',
'proguard-rounded-image-view.pro',
'proguard-glide.pro',
'proguard-shortcutbadger.pro',
'proguard-retrofit.pro',
'proguard-webrtc.pro',
'proguard-klinker.pro',
'proguard-retrolambda.pro',
'proguard-okhttp.pro',
'proguard-ez-vcard.pro',
'proguard.cfg'
testProguardFiles 'proguard-automation.pro',
'proguard.cfg'
}
release {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
}
}
productFlavors {
play {
dimension "none"
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
website {
dimension "none"
ext.websiteUpdateUrl = "https://updates.signal.org/android"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
}
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
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']
}
androidTest {
java.srcDirs = ['test/androidTest/java']
}
test {
java.srcDirs = ['test/unitTest/java']
resources.srcDirs = ['test/unitTest/resources']
}
website.manifest.srcFile 'website/AndroidManifest.xml'
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
def assembleWebsiteDescriptor = { variant, file ->
if (file.exists()) {
MessageDigest md = MessageDigest.getInstance("SHA-256");
file.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size);
}
String digest = md.digest().collect {String.format "%02x", it}.join();
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl
String apkName = file.getName()
String descriptor = "{" +
"\"versionCode\" : $canonicalVersionCode," +
"\"versionName\" : \"$canonicalVersionName\"," +
"\"sha256sum\" : \"$digest\"," +
"\"url\" : \"$url/$apkName\"" +
"}"
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
descriptorFile.write(descriptor)
}
}
def signProductionRelease = { variant ->
variant.outputs.collect { output ->
String apkName = output.outputFile.name
File inputFile = new File(output.outputFile.path)
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
'pkcs11.config',
'PKCS11',
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
outputFile.getAbsolutePath())
inputFile.delete()
outputFile
}
}
task signProductionPlayRelease {
doLast {
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
}
}
task signProductionWebsiteRelease {
doLast {
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
assembleWebsiteDescriptor(variant, signedRelease)
}
}
tasks.whenTaskAdded { task ->
if (task.name.equals("assemblePlayRelease")) {
task.finalizedBy signProductionPlayRelease
}
if (task.name.equals("assembleWebsiteRelease")) {
task.finalizedBy signProductionWebsiteRelease
}
}
def getLastCommitTimestamp() {
new ByteArrayOutputStream().withStream { os ->
def result = exec {
executable = 'git'
args = ['log', '-1', '--pretty=format:%ct']
standardOutput = os
}
return os.toString() + "000"
}
}
/**
* Discovers supported languages listed as under the res/values- directory.
*/
static def autoResConfig() {
def files = new ArrayList<String>()
def root = new File('res')
root.eachFile { f -> files.add(f.name) }
['en'] + files.collect { f -> f =~ /^values-([a-z]{2}(-r[A-Z]{2})?)$/ }
.findAll { matcher -> matcher.find() }
.collect { matcher -> matcher.group(1) }
.sort()
}
task qa {
group 'Verification'
description 'Quality Assurance. Run before pushing.'
dependsOn ':testPlayReleaseUnitTest', ':lintPlayRelease', ':assemblePlayDebug'
}

View File

@@ -5,5 +5,5 @@ repositories {
}
dependencies {
compile 'com.android.tools.build:apksig:3.3.2'
implementation 'com.android.tools.build:apksig:3.5.1'
}

View File

@@ -3,7 +3,6 @@ package org.whispersystems.witness
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ResolvedArtifact
import java.security.MessageDigest
@@ -28,10 +27,10 @@ class WitnessPlugin implements Plugin<Project> {
project.afterEvaluate {
project.dependencyVerification.verify.each {
assertion ->
List parts = assertion.tokenize(":")
List parts = assertion[0].tokenize(':')
String group = parts.get(0)
String name = parts.get(1)
String hash = parts.get(2)
String hash = assertion[1]
def artifacts = allArtifacts(project).findAll {
return it.name.equals(name) && it.moduleVersion.id.group.equals(group)
@@ -56,22 +55,25 @@ class WitnessPlugin implements Plugin<Project> {
}
project.task('calculateChecksums').doLast {
println "dependencyVerification {"
def stringBuilder = new StringBuilder()
def configurationName = project.dependencyVerification.configuration
if (configurationName != null) {
println " configuration = '$configurationName'"
}
stringBuilder.append '// Auto-generated, use ./gradlew calculateChecksums to regenerate\n\n'
stringBuilder.append 'dependencyVerification {\n'
println " verify = ["
stringBuilder.append ' verify = [\n'
allArtifacts(project).each {
dep ->
println " '" + dep.moduleVersion.id.group+ ":" + dep.name + ":" + calculateSha256(dep.file) + "',"
}
allArtifacts(project)
.findAll { dep -> !dep.id.componentIdentifier.displayName.startsWith('project :') }
.collect { dep -> "['$dep.moduleVersion.id.group:$dep.name:$dep.moduleVersion.id.version',\n '${calculateSha256(dep.file)}']" }
.sort()
.each {
dep -> stringBuilder.append "\n $dep,\n"
}
println " ]"
println "}"
stringBuilder.append " ]\n"
stringBuilder.append "}\n"
project.file("witness-verifications.gradle").write(stringBuilder.toString())
}
}

Binary file not shown.

View File

@@ -1,5 +1,7 @@
# Note: Check https://gradle.org/release-checksums/ before updating wrapper or distribution
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
distributionSha256Sum=027fdd265d277bae65a0d349b6b8da02135b0b8e14ba891e26281fa877fe37a2
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

22
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -109,8 +125,8 @@ 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
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

18
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@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="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

1
libsignal/service/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,155 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
}
apply plugin: 'java-library'
apply plugin: 'com.google.protobuf'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: 'witness'
apply from: 'witness-verifications.gradle'
sourceCompatibility = 1.7
archivesBaseName = "signal-service-java"
version = lib_signal_service_version_number
group = lib_signal_service_group_info
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
api 'com.googlecode.libphonenumber:libphonenumber:8.11.0'
api 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2'
api "org.signal:signal-metadata-java:${lib_signal_metadata_version}"
api 'com.squareup.okhttp3:okhttp:3.12.1'
implementation 'org.threeten:threetenbp:1.3.6'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'
}
dependencyVerification {
configuration = '(runtime|compile)Classpath'
}
tasks.whenTaskAdded { task ->
if (task.name.equals("lint")) {
task.enabled = false
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.10.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
sourceSets {
main.proto.srcDir 'protobuf'
}
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return hasProperty('sonatypeRepo') ? sonatypeRepo
: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
def getRepositoryUsername() {
return hasProperty('whisperSonatypeUsername') ? whisperSonatypeUsername : ""
}
def getRepositoryPassword() {
return hasProperty('whisperSonatypePassword') ? whisperSonatypePassword : ""
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
sign configurations.archives
}
uploadArchives {
configuration = configurations.archives
repositories.mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: getReleaseRepositoryUrl()) {
authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
}
pom.project {
name 'signal-service-java'
packaging 'jar'
description 'Signal Service communication library for Java'
url 'https://github.com/WhisperSystems/libsignal-service-java'
scm {
url 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
connection 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
developerConnection 'scm:git@github.com:WhisperSystems/libsignal-service-java.git'
}
licenses {
license {
name 'GPLv3'
url 'https://www.gnu.org/licenses/gpl-3.0.txt'
distribution 'repo'
}
}
developers {
developer {
name 'Moxie Marlinspike'
}
}
}
}
}
task installArchives(type: Upload) {
description "Installs the artifacts to the local Maven repository."
configuration = configurations['archives']
repositories {
mavenDeployer {
repository url: "file://${System.properties['user.home']}/.m2/repository"
}
}
}
task packageJavadoc(type: Jar, dependsOn: 'javadoc') {
from javadoc.destinationDir
classifier = 'javadoc'
}
task packageSources(type: Jar) {
from sourceSets.main.allSource
classifier = 'sources'
}
artifacts {
archives packageJavadoc
archives packageSources
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package textsecure;
option java_package = "org.whispersystems.signalservice.internal.keybackup.protos";
option java_multiple_files = true;
message Request {
optional BackupRequest backup = 1;
optional RestoreRequest restore = 2;
optional DeleteRequest delete = 3;
}
message Response {
optional BackupResponse backup = 1;
optional RestoreResponse restore = 2;
optional DeleteResponse delete = 3;
}
message BackupRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
optional bytes token = 3;
optional uint64 valid_from = 4;
optional bytes data = 5;
optional bytes pin = 6;
optional uint32 tries = 7;
}
message BackupResponse {
enum Status {
OK = 1;
ALREADY_EXISTS = 2;
NOT_YET_VALID = 3;
}
optional Status status = 1;
optional bytes token = 2;
}
message RestoreRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
optional bytes token = 3;
optional uint64 valid_from = 4;
optional bytes pin = 5;
}
message RestoreResponse {
enum Status {
OK = 1;
TOKEN_MISMATCH = 2;
NOT_YET_VALID = 3;
MISSING = 4;
PIN_MISMATCH = 5;
}
optional Status status = 1;
optional bytes token = 2;
optional bytes data = 3;
optional uint32 tries = 4;
}
message DeleteRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
}
message DeleteResponse {
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "ProvisioningProtos";
message ProvisionEnvelope {
optional bytes publicKey = 1;
optional bytes body = 2; // Encrypted ProvisionMessage
}
message ProvisionMessage {
optional bytes identityKeyPublic = 1;
optional bytes identityKeyPrivate = 2;
optional string number = 3;
optional string uuid = 8;
optional string provisioningCode = 4;
optional string userAgent = 5;
optional bytes profileKey = 6;
optional bool readReceipts = 7;
optional uint32 provisioningVersion = 9;
}
enum ProvisioningVersion {
option allow_alias = true;
INITIAL = 0;
TABLET_SUPPORT = 1;
CURRENT = 1;
}

View File

@@ -0,0 +1,436 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.push";
option java_outer_classname = "SignalServiceProtos";
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
}
optional Type type = 1;
optional string sourceE164 = 2;
optional string sourceUuid = 11;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
optional uint64 serverTimestamp = 10;
}
message Content {
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
}
message CallMessage {
message Offer {
optional uint64 id = 1;
optional string description = 2;
}
message Answer {
optional uint64 id = 1;
optional string description = 2;
}
message IceUpdate {
optional uint64 id = 1;
optional string sdpMid = 2;
optional uint32 sdpMLineIndex = 3;
optional string sdp = 4;
}
message Busy {
optional uint64 id = 1;
}
message Hangup {
optional uint64 id = 1;
}
optional Offer offer = 1;
optional Answer answer = 2;
repeated IceUpdate iceUpdate = 3;
optional Hangup hangup = 4;
optional Busy busy = 5;
}
message DataMessage {
enum Flags {
END_SESSION = 1;
EXPIRATION_TIMER_UPDATE = 2;
PROFILE_KEY_UPDATE = 4;
}
message Quote {
message QuotedAttachment {
optional string contentType = 1;
optional string fileName = 2;
optional AttachmentPointer thumbnail = 3;
}
optional uint64 id = 1;
optional string authorE164 = 2;
optional string authorUuid = 5;
optional string text = 3;
repeated QuotedAttachment attachments = 4;
}
message Contact {
message Name {
optional string givenName = 1;
optional string familyName = 2;
optional string prefix = 3;
optional string suffix = 4;
optional string middleName = 5;
optional string displayName = 6;
}
message Phone {
enum Type {
HOME = 1;
MOBILE = 2;
WORK = 3;
CUSTOM = 4;
}
optional string value = 1;
optional Type type = 2;
optional string label = 3;
}
message Email {
enum Type {
HOME = 1;
MOBILE = 2;
WORK = 3;
CUSTOM = 4;
}
optional string value = 1;
optional Type type = 2;
optional string label = 3;
}
message PostalAddress {
enum Type {
HOME = 1;
WORK = 2;
CUSTOM = 3;
}
optional Type type = 1;
optional string label = 2;
optional string street = 3;
optional string pobox = 4;
optional string neighborhood = 5;
optional string city = 6;
optional string region = 7;
optional string postcode = 8;
optional string country = 9;
}
message Avatar {
optional AttachmentPointer avatar = 1;
optional bool isProfile = 2;
}
optional Name name = 1;
repeated Phone number = 3;
repeated Email email = 4;
repeated PostalAddress address = 5;
optional Avatar avatar = 6;
optional string organization = 7;
}
message Preview {
optional string url = 1;
optional string title = 2;
optional AttachmentPointer image = 3;
}
message Sticker {
optional bytes packId = 1;
optional bytes packKey = 2;
optional uint32 stickerId = 3;
optional AttachmentPointer data = 4;
}
message Reaction {
optional string emoji = 1;
optional bool remove = 2;
optional string targetAuthorE164 = 3;
optional string targetAuthorUuid = 4;
optional uint64 targetSentTimestamp = 5;
}
enum ProtocolVersion {
option allow_alias = true;
INITIAL = 0;
MESSAGE_TIMERS = 1;
VIEW_ONCE = 2;
VIEW_ONCE_VIDEO = 3;
REACTIONS = 4;
CURRENT = 4;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Contact contact = 9;
repeated Preview preview = 10;
optional Sticker sticker = 11;
optional uint32 requiredProtocolVersion = 12;
optional bool isViewOnce = 14;
optional Reaction reaction = 16;
}
message NullMessage {
optional bytes padding = 1;
}
message ReceiptMessage {
enum Type {
DELIVERY = 0;
READ = 1;
}
optional Type type = 1;
repeated uint64 timestamp = 2;
}
message TypingMessage {
enum Action {
STARTED = 0;
STOPPED = 1;
}
optional uint64 timestamp = 1;
optional Action action = 2;
optional bytes groupId = 3;
}
message Verified {
enum State {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional string destinationE164 = 1;
optional string destinationUuid = 5;
optional bytes identityKey = 2;
optional State state = 3;
optional bytes nullMessage = 4;
}
message SyncMessage {
message Sent {
message UnidentifiedDeliveryStatus {
optional string destinationE164 = 1;
optional string destinationUuid = 3;
optional bool unidentified = 2;
}
optional string destinationE164 = 1;
optional string destinationUuid = 7;
optional uint64 timestamp = 2;
optional DataMessage message = 3;
optional uint64 expirationStartTimestamp = 4;
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
optional bool isRecipientUpdate = 6 [default = false];
}
message Contacts {
optional AttachmentPointer blob = 1;
optional bool complete = 2 [default = false];
}
message Groups {
optional AttachmentPointer blob = 1;
}
message Blocked {
repeated string numbers = 1;
repeated string uuids = 3;
repeated bytes groupIds = 2;
}
message Request {
enum Type {
UNKNOWN = 0;
CONTACTS = 1;
GROUPS = 2;
BLOCKED = 3;
CONFIGURATION = 4;
KEYS = 5;
}
optional Type type = 1;
}
message Read {
optional string senderE164 = 1;
optional string senderUuid = 3;
optional uint64 timestamp = 2;
}
message Configuration {
optional bool readReceipts = 1;
optional bool unidentifiedDeliveryIndicators = 2;
optional bool typingIndicators = 3;
optional bool linkPreviews = 4;
optional uint32 provisioningVersion = 5;
}
message StickerPackOperation {
enum Type {
INSTALL = 0;
REMOVE = 1;
}
optional bytes packId = 1;
optional bytes packKey = 2;
optional Type type = 3;
}
message ViewOnceOpen {
optional string senderE164 = 1;
optional string senderUuid = 3;
optional uint64 timestamp = 2;
}
message FetchLatest {
enum Type {
UNKNOWN = 0;
LOCAL_PROFILE = 1;
STORAGE_MANIFEST = 2;
}
optional Type type = 1;
}
message Keys {
optional bytes storageService = 1;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
optional Request request = 4;
repeated Read read = 5;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional Configuration configuration = 9;
optional bytes padding = 8;
repeated StickerPackOperation stickerPackOperation = 10;
optional ViewOnceOpen viewOnceOpen = 11;
optional FetchLatest fetchLatest = 12;
optional Keys keys = 13;
}
message AttachmentPointer {
enum Flags {
VOICE_MESSAGE = 1;
}
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
optional uint32 size = 4;
optional bytes thumbnail = 5;
optional bytes digest = 6;
optional string fileName = 7;
optional uint32 flags = 8;
optional uint32 width = 9;
optional uint32 height = 10;
optional string caption = 11;
optional string blurHash = 12;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
REQUEST_INFO = 4;
}
message Member {
optional string uuid = 1;
optional string e164 = 2;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string membersE164 = 4;
repeated Member members = 6;
optional AttachmentPointer avatar = 5;
}
message ContactDetails {
message Avatar {
optional string contentType = 1;
optional uint32 length = 2;
}
optional string number = 1;
optional string uuid = 9;
optional string name = 2;
optional Avatar avatar = 3;
optional string color = 4;
optional Verified verified = 5;
optional bytes profileKey = 6;
optional bool blocked = 7;
optional uint32 expireTimer = 8;
}
message GroupDetails {
message Avatar {
optional string contentType = 1;
optional uint32 length = 2;
}
message Member {
optional string uuid = 1;
optional string e164 = 2;
}
optional bytes id = 1;
optional string name = 2;
repeated string membersE164 = 3;
repeated Member members = 9;
optional Avatar avatar = 4;
optional bool active = 5 [default = true];
optional uint32 expireTimer = 6;
optional string color = 7;
optional bool blocked = 8;
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.storage.protos";
option java_multiple_files = true;
message StorageItem {
optional bytes key = 1;
optional bytes value = 2;
}
message StorageItems {
repeated StorageItem items = 1;
}
message StorageManifest {
optional uint64 version = 1;
optional bytes value = 2;
}
message ReadOperation {
repeated bytes readKey = 1;
}
message WriteOperation {
optional StorageManifest manifest = 1;
repeated StorageItem insertItem = 2;
repeated bytes deleteKey = 3;
}
message StorageRecord {
enum Type {
UNKNOWN = 0;
CONTACT = 1;
}
optional uint32 type = 1;
optional ContactRecord contact = 2;
}
message ContactRecord {
message Identity {
enum State {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional bytes key = 1;
optional State state = 2;
}
message Profile {
optional string name = 1;
optional bytes key = 2;
optional string username = 3;
}
optional string serviceUuid = 1;
optional string serviceE164 = 2;
optional Profile profile = 3;
optional Identity identity = 4;
optional bool blocked = 5;
optional bool whitelisted = 6;
optional string nickname = 7;
}
message ManifestRecord {
optional uint64 version = 1;
repeated bytes keys = 2;
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.sticker";
option java_outer_classname = "StickerProtos";
message Pack {
message Sticker {
optional uint32 id = 1;
optional string emoji = 2;
}
optional string title = 1;
optional string author = 2;
optional Sticker cover = 3;
repeated Sticker stickers = 4;
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.websocket";
option java_outer_classname = "WebSocketProtos";
message WebSocketRequestMessage {
optional string verb = 1;
optional string path = 2;
optional bytes body = 3;
repeated string headers = 5;
optional uint64 id = 4;
}
message WebSocketResponseMessage {
optional uint64 id = 1;
optional uint32 status = 2;
optional string message = 3;
repeated string headers = 5;
optional bytes body = 4;
}
message WebSocketMessage {
enum Type {
UNKNOWN = 0;
REQUEST = 1;
RESPONSE = 2;
}
optional Type type = 1;
optional WebSocketRequestMessage request = 2;
optional WebSocketResponseMessage response = 3;
}

View File

@@ -0,0 +1,281 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.util.Locale;
public final class KeyBackupService {
private static final String TAG = KeyBackupService.class.getSimpleName();
private final KeyStore iasKeyStore;
private final String enclaveName;
private final String mrenclave;
private final PushServiceSocket pushServiceSocket;
private final int maxTries;
KeyBackupService(KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
PushServiceSocket pushServiceSocket,
int maxTries)
{
this.iasKeyStore = iasKeyStore;
this.enclaveName = enclaveName;
this.mrenclave = mrenclave;
this.pushServiceSocket = pushServiceSocket;
this.maxTries = maxTries;
}
/**
* Use this if you don't want to validate that the server has not changed since you last set the pin.
*/
public PinChangeSession newPinChangeSession()
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), null);
}
/**
* Use this if you want to validate that the server has not changed since you last set the pin.
* The supplied token will have to match for the change to be successful.
*/
public PinChangeSession newPinChangeSession(TokenResponse currentToken)
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
}
/**
* Use this to validate that the pin is still set on the server with the current token.
* Additionally this validates that no one has used any tries.
*/
public RestoreSession newRestoreSession(TokenResponse currentToken)
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
}
/**
* Only call before registration, to see how many tries are left.
* <p>
* Pass the token to the newRegistrationSession.
*/
public TokenResponse getToken(String authAuthorization) throws IOException {
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
}
/**
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
*
* @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks.
*/
public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse)
throws IOException
{
return newSession(authAuthorization, tokenResponse);
}
private Session newSession(String authorization, TokenResponse currentToken)
throws IOException
{
TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName);
return new Session(authorization, token);
}
private class Session implements RestoreSession, PinChangeSession {
private final String authorization;
private final TokenResponse currentToken;
Session(String authorization, TokenResponse currentToken) {
this.authorization = authorization;
this.currentToken = currentToken;
}
@Override
public RegistrationLockData restorePin(String pin)
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException
{
int attempt = 0;
SecureRandom random = new SecureRandom();
TokenResponse token = currentToken;
while (true) {
attempt++;
try {
return restorePin(pin, token);
} catch (TokenException tokenException) {
token = tokenException.getToken();
if (tokenException instanceof KeyBackupServicePinException) {
throw (KeyBackupServicePinException) tokenException;
}
if (tokenException.isCanAutomaticallyRetry() && attempt < 5) {
// back off randomly, between 250 and 8000 ms
int backoffMs = 250 * (1 << (attempt - 1));
Util.sleep(backoffMs + random.nextInt(backoffMs));
} else {
throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries");
}
}
}
}
private RegistrationLockData restorePin(String pin, TokenResponse token)
throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException
{
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
try {
final int remainingTries = token.getTries();
final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
TokenResponse nextToken = status.hasToken()
? new TokenResponse(token.getBackupId(), status.getToken().toByteArray(), status.getTries())
: token;
Log.i(TAG, "Restore " + status.getStatus());
switch (status.getStatus()) {
case OK:
Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries()));
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray());
return new RegistrationLockData(masterKey, nextToken);
case PIN_MISMATCH:
Log.i(TAG, "Restore PIN_MISMATCH");
throw new KeyBackupServicePinException(nextToken);
case TOKEN_MISMATCH:
Log.i(TAG, "Restore TOKEN_MISMATCH");
// if the number of tries has not fallen, the pin is correct we're just using an out of date token
boolean canRetry = remainingTries == status.getTries();
Log.i(TAG, String.format(Locale.US, "Token MISMATCH %d %d", remainingTries, status.getTries()));
Log.i(TAG, String.format("Last token %s", Hex.toStringCondensed(token.getToken())));
Log.i(TAG, String.format("Next token %s", Hex.toStringCondensed(nextToken.getToken())));
throw new TokenException(nextToken, canRetry);
case MISSING:
Log.i(TAG, "Restore OK! No data though");
return null;
case NOT_YET_VALID:
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
}
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
return null;
}
private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException {
try {
return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization);
} catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) {
throw new UnauthenticatedResponseException(e);
}
}
@Override
public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException {
PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin)
.withNewSecurePinKey2();
TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(),
masterKey.getPinKey2(),
enclaveName,
currentToken);
pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock());
return new RegistrationLockData(masterKey, tokenResponse);
}
@Override
public void removePin() throws IOException, UnauthenticatedResponseException {
deleteKbsData();
pushServiceSocket.removePinV2();
}
private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token)
throws IOException, UnauthenticatedResponseException
{
try {
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, Hex.fromStringCondensed(enclaveName), maxTries);
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation);
BackupResponse.Status status = backupResponse.getStatus();
switch (status) {
case OK:
return backupResponse.hasToken() ? new TokenResponse(token.getBackupId(), backupResponse.getToken().toByteArray(), maxTries) : token;
case ALREADY_EXISTS:
throw new UnauthenticatedResponseException("Already exists");
case NOT_YET_VALID:
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
default:
throw new AssertionError("Unknown response status " + status);
}
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
private void deleteKbsData()
throws IOException, UnauthenticatedResponseException
{
try {
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, Hex.fromStringCondensed(enclaveName));
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation);
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
}
public interface RestoreSession {
RegistrationLockData restorePin(String pin)
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException;
}
public interface PinChangeSession {
RegistrationLockData setPin(String pin)
throws IOException, UnauthenticatedResponseException, InvalidPinException;
void removePin()
throws IOException, UnauthenticatedResponseException;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
public final class KeyBackupServicePinException extends TokenException {
private final int triesRemaining;
public KeyBackupServicePinException(TokenResponse nextToken) {
super(nextToken, false);
this.triesRemaining = nextToken.getTries();
}
public int getTriesRemaining() {
return triesRemaining;
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
public final class RegistrationLockData {
private final PinStretcher.MasterKey masterKey;
private final TokenResponse tokenResponse;
RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) {
this.masterKey = masterKey;
this.tokenResponse = tokenResponse;
}
public PinStretcher.MasterKey getMasterKey() {
return masterKey;
}
public TokenResponse getTokenResponse() {
return tokenResponse;
}
public int getRemainingTries() {
return tokenResponse.getTries();
}
}

View File

@@ -0,0 +1,624 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
import org.whispersystems.signalservice.internal.storage.protos.StorageItems;
import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage;
import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisioningVersion;
/**
* The main interface for creating, registering, and
* managing a Signal Service account.
*
* @author Moxie Marlinspike
*/
public class SignalServiceAccountManager {
private static final String TAG = SignalServiceAccountManager.class.getSimpleName();
private final PushServiceSocket pushServiceSocket;
private final CredentialsProvider credentials;
private final String userAgent;
/**
* Construct a SignalServiceAccountManager.
*
* @param configuration The URL for the Signal Service.
* @param uuid The Signal Service UUID.
* @param e164 The Signal Service phone number.
* @param password A Signal Service password.
* @param userAgent A string which identifies the client software.
*/
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
UUID uuid, String e164, String password,
String userAgent)
{
this(configuration, new StaticCredentialsProvider(uuid, e164, password, null), userAgent);
}
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
CredentialsProvider credentialsProvider,
String userAgent)
{
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, userAgent);
this.credentials = credentialsProvider;
this.userAgent = userAgent;
}
public byte[] getSenderCertificate() throws IOException {
return this.pushServiceSocket.getSenderCertificate();
}
public byte[] getSenderCertificateLegacy() throws IOException {
return this.pushServiceSocket.getSenderCertificateLegacy();
}
/**
* @deprecated Remove this method once KBS is live.
*/
@Deprecated
public void setPin(String pin) throws IOException {
this.pushServiceSocket.setPin(pin);
}
/**
* V1 Pin setting has been replaced by KeyBackupService.
* Now you can only remove the old pin but there is no need to remove the old pin if setting a KBS Pin.
*/
public void removeV1Pin() throws IOException {
this.pushServiceSocket.removePin();
}
public UUID getOwnUuid() throws IOException {
return this.pushServiceSocket.getOwnUuid();
}
public KeyBackupService getKeyBackupService(KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
int tries)
{
return new KeyBackupService(iasKeyStore, enclaveName, mrenclave, pushServiceSocket, tries);
}
/**
* Register/Unregister a Google Cloud Messaging registration ID.
*
* @param gcmRegistrationId The GCM id to register. A call with an absent value will unregister.
* @throws IOException
*/
public void setGcmId(Optional<String> gcmRegistrationId) throws IOException {
if (gcmRegistrationId.isPresent()) {
this.pushServiceSocket.registerGcmId(gcmRegistrationId.get());
} else {
this.pushServiceSocket.unregisterGcmId();
}
}
/**
* Request a push challenge. A number will be pushed to the GCM (FCM) id. This can then be used
* during SMS/call requests to bypass the CAPTCHA.
*
* @param gcmRegistrationId The GCM (FCM) id to use.
* @param e164number The number to associate it with.
* @throws IOException
*/
public void requestPushChallenge(String gcmRegistrationId, String e164number) throws IOException {
this.pushServiceSocket.requestPushChallenge(gcmRegistrationId, e164number);
}
/**
* Request an SMS verification code. On success, the server will send
* an SMS verification code to this Signal user.
*
* @param androidSmsRetrieverSupported
* @param captchaToken If the user has done a CAPTCHA, include this.
* @param challenge If present, it can bypass the CAPTCHA.
* @throws IOException
*/
public void requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge);
}
/**
* Request a Voice verification code. On success, the server will
* make a voice call to this Signal user.
*
* @param locale
* @param captchaToken If the user has done a CAPTCHA, include this.
* @param challenge If present, it can bypass the CAPTCHA.
* @throws IOException
*/
public void requestVoiceVerificationCode(Locale locale, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge);
}
/**
* Verify a Signal Service account with a received SMS or voice verification code.
*
* @param verificationCode The verification code received via SMS or Voice
* (see {@link #requestSmsVerificationCode} and
* {@link #requestVoiceVerificationCode}).
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key,
* concatenated.
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
* @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
* @param registrationLock Only supply if found on KBS.
* @return The UUID of the user that was registered.
* @throws IOException
*/
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
signalProtocolRegistrationId,
fetchesMessages,
pin, registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess);
}
/**
* Refresh account attributes with server.
*
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, concatenated.
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
* This value should remain consistent across registrations for the same
* install, but probabilistically differ across registrations for
* separate installs.
* @param pin Only supply if pin has not yet been migrated to KBS.
* @param registrationLock Only supply if found on KBS.
*
* @throws IOException
*/
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages,
pin, registrationLock,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
}
/**
* Register an identity key, signed prekey, and list of one time prekeys
* with the server.
*
* @param identityKey The client's long-term identity keypair.
* @param signedPreKey The client's signed prekey.
* @param oneTimePreKeys The client's list of one-time prekeys.
*
* @throws IOException
*/
public void setPreKeys(IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
throws IOException
{
this.pushServiceSocket.registerPreKeys(identityKey, signedPreKey, oneTimePreKeys);
}
/**
* @return The server's count of currently available (eg. unused) prekeys for this user.
* @throws IOException
*/
public int getPreKeysCount() throws IOException {
return this.pushServiceSocket.getAvailablePreKeys();
}
/**
* Set the client's signed prekey.
*
* @param signedPreKey The client's new signed prekey.
* @throws IOException
*/
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey);
}
/**
* @return The server's view of the client's current signed prekey.
* @throws IOException
*/
public SignedPreKeyEntity getSignedPreKey() throws IOException {
return this.pushServiceSocket.getCurrentSignedPreKey();
}
/**
* Checks whether a contact is currently registered with the server.
*
* @param e164number The contact to check.
* @return An optional ContactTokenDetails, present if registered, absent if not.
* @throws IOException
*/
public Optional<ContactTokenDetails> getContact(String e164number) throws IOException {
String contactToken = createDirectoryServerToken(e164number, true);
ContactTokenDetails contactTokenDetails = this.pushServiceSocket.getContactTokenDetails(contactToken);
if (contactTokenDetails != null) {
contactTokenDetails.setNumber(e164number);
}
return Optional.fromNullable(contactTokenDetails);
}
/**
* Checks which contacts in a set are registered with the server.
*
* @param e164numbers The contacts to check.
* @return A list of ContactTokenDetails for the registered users.
* @throws IOException
*/
public List<ContactTokenDetails> getContacts(Set<String> e164numbers)
throws IOException
{
Map<String, String> contactTokensMap = createDirectoryServerTokenMap(e164numbers);
List<ContactTokenDetails> activeTokens = this.pushServiceSocket.retrieveDirectory(contactTokensMap.keySet());
for (ContactTokenDetails activeToken : activeTokens) {
activeToken.setNumber(contactTokensMap.get(activeToken.getToken()));
}
return activeTokens;
}
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String enclaveId)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
try {
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
List<String> addressBook = new LinkedList<>();
for (String e164number : e164numbers) {
addressBook.add(e164number.substring(1));
}
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
Iterator<String> addressBookIterator = addressBook.iterator();
List<String> results = new LinkedList<>();
for (byte aData : data) {
String candidate = addressBookIterator.next();
if (aData != 0) results.add('+' + candidate);
}
return results;
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
public void reportContactDiscoveryServiceMatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result match failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceMismatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMismatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result mismatch failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceAttestationError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceAttestationError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery attestation error failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceUnexpectedError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceUnexpectedError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery unexpected error failed. Ignoring.", e);
}
}
public long getStorageManifestVersion() throws IOException {
try {
String authToken = this.pushServiceSocket.getStorageAuth();
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
return storageManifest.getVersion();
} catch (NotFoundException e) {
return 0;
}
}
public Optional<SignalStorageManifest> getStorageManifest(byte[] storageServiceKey) throws IOException, InvalidKeyException {
try {
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
String authToken = this.pushServiceSocket.getStorageAuth();
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
byte[] rawRecord = cipher.decrypt(storageManifest.getValue().toByteArray());
ManifestRecord manifestRecord = ManifestRecord.parseFrom(rawRecord);
List<byte[]> keys = new ArrayList<>(manifestRecord.getKeysCount());
for (ByteString key : manifestRecord.getKeysList()) {
keys.add(key.toByteArray());
}
return Optional.of(new SignalStorageManifest(manifestRecord.getVersion(), keys));
} catch (NotFoundException e) {
return Optional.absent();
}
}
public List<SignalStorageRecord> readStorageRecords(byte[] storageServiceKey, List<byte[]> storageKeys) throws IOException, InvalidKeyException {
ReadOperation.Builder operation = ReadOperation.newBuilder();
for (byte[] key : storageKeys) {
operation.addReadKey(ByteString.copyFrom(key));
}
String authToken = this.pushServiceSocket.getStorageAuth();
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
SignalStorageCipher storageCipher = new SignalStorageCipher(storageServiceKey);
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
for (StorageItem item : items.getItemsList()) {
if (item.hasKey()) {
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, storageCipher));
} else {
Log.w(TAG, "Encountered a StorageItem with no key! Skipping.");
}
}
return result;
}
/**
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
*/
public Optional<SignalStorageManifest> writeStorageRecords(byte[] storageServiceKey,
SignalStorageManifest manifest,
List<SignalStorageRecord> inserts,
List<byte[]> deletes)
throws IOException, InvalidKeyException
{
ManifestRecord.Builder manifestRecordBuilder = ManifestRecord.newBuilder().setVersion(manifest.getVersion());
for (byte[] key : manifest.getStorageKeys()) {
manifestRecordBuilder.addKeys(ByteString.copyFrom(key));
}
String authToken = this.pushServiceSocket.getStorageAuth();
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
byte[] encryptedRecord = cipher.encrypt(manifestRecordBuilder.build().toByteArray());
StorageManifest storageManifest = StorageManifest.newBuilder()
.setVersion(manifest.getVersion())
.setValue(ByteString.copyFrom(encryptedRecord))
.build();
WriteOperation.Builder writeBuilder = WriteOperation.newBuilder().setManifest(storageManifest);
for (SignalStorageRecord insert : inserts) {
writeBuilder.addInsertItem(SignalStorageModels.localToRemoteStorageRecord(insert, cipher));
}
for (byte[] delete : deletes) {
writeBuilder.addDeleteKey(ByteString.copyFrom(delete));
}
Optional<StorageManifest> conflict = this.pushServiceSocket.writeStorageContacts(authToken, writeBuilder.build());
if (conflict.isPresent()) {
byte[] rawManifestRecord = cipher.decrypt(conflict.get().getValue().toByteArray());
ManifestRecord record = ManifestRecord.parseFrom(rawManifestRecord);
List<byte[]> keys = new ArrayList<>(record.getKeysCount());
for (ByteString key : record.getKeysList()) {
keys.add(key.toByteArray());
}
SignalStorageManifest conflictManifest = new SignalStorageManifest(record.getVersion(), keys);
return Optional.of(conflictManifest);
} else {
return Optional.absent();
}
}
public String getNewDeviceVerificationCode() throws IOException {
return this.pushServiceSocket.getNewDeviceVerificationCode();
}
public void addDevice(String deviceIdentifier,
ECPublicKey deviceKey,
IdentityKeyPair identityKeyPair,
Optional<byte[]> profileKey,
String code)
throws InvalidKeyException, IOException
{
ProvisioningCipher cipher = new ProvisioningCipher(deviceKey);
ProvisionMessage.Builder message = ProvisionMessage.newBuilder()
.setIdentityKeyPublic(ByteString.copyFrom(identityKeyPair.getPublicKey().serialize()))
.setIdentityKeyPrivate(ByteString.copyFrom(identityKeyPair.getPrivateKey().serialize()))
.setProvisioningCode(code)
.setProvisioningVersion(ProvisioningVersion.CURRENT_VALUE);
String e164 = credentials.getE164();
UUID uuid = credentials.getUuid();
if (e164 != null) {
message.setNumber(e164);
} else {
throw new AssertionError("Missing phone number!");
}
if (uuid != null) {
message.setUuid(uuid.toString());
} else {
Log.w(TAG, "[addDevice] Missing UUID.");
}
if (profileKey.isPresent()) {
message.setProfileKey(ByteString.copyFrom(profileKey.get()));
}
byte[] ciphertext = cipher.encrypt(message.build());
this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext);
}
public List<DeviceInfo> getDevices() throws IOException {
return this.pushServiceSocket.getDevices();
}
public void removeDevice(long deviceId) throws IOException {
this.pushServiceSocket.removeDevice(deviceId);
}
public TurnServerInfo getTurnServerInfo() throws IOException {
return this.pushServiceSocket.getTurnServerInfo();
}
public void setProfileName(byte[] key, String name)
throws IOException
{
if (name == null) name = "";
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes("UTF-8"), ProfileCipher.NAME_PADDED_LENGTH));
this.pushServiceSocket.setProfileName(ciphertextName);
}
public void setProfileAvatar(byte[] key, StreamDetails avatar)
throws IOException
{
ProfileAvatarData profileAvatarData = null;
if (avatar != null) {
profileAvatarData = new ProfileAvatarData(avatar.getStream(),
ProfileCipherOutputStream.getCiphertextLength(avatar.getLength()),
avatar.getContentType(),
new ProfileCipherOutputStreamFactory(key));
}
this.pushServiceSocket.setProfileAvatar(profileAvatarData);
}
public void setUsername(String username) throws IOException {
this.pushServiceSocket.setUsername(username);
}
public void deleteUsername() throws IOException {
this.pushServiceSocket.deleteUsername();
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
this.pushServiceSocket.setSoTimeoutMillis(soTimeoutMillis);
}
public void cancelInFlightRequests() {
this.pushServiceSocket.cancelInFlightRequests();
}
private String createDirectoryServerToken(String e164number, boolean urlSafe) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");
byte[] token = Util.trim(digest.digest(e164number.getBytes()), 10);
String encoded = Base64.encodeBytesWithoutPadding(token);
if (urlSafe) return encoded.replace('+', '-').replace('/', '_');
else return encoded;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Map<String, String> createDirectoryServerTokenMap(Collection<String> e164numbers) {
Map<String,String> tokenMap = new HashMap<>(e164numbers.size());
for (String number : e164numbers) {
tokenMap.put(createDirectoryServerToken(number, false), number);
}
return tokenMap;
}
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage;
import static org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketResponseMessage;
/**
* A SignalServiceMessagePipe represents a dedicated connection
* to the Signal Service, which the server can push messages
* down through.
*/
public class SignalServiceMessagePipe {
private static final String TAG = SignalServiceMessagePipe.class.getName();
private final WebSocketConnection websocket;
private final Optional<CredentialsProvider> credentialsProvider;
SignalServiceMessagePipe(WebSocketConnection websocket, Optional<CredentialsProvider> credentialsProvider) {
this.websocket = websocket;
this.credentialsProvider = credentialsProvider;
this.websocket.connect();
}
/**
* A blocking call that reads a message off the pipe. When this
* call returns, the message has been acknowledged and will not
* be retransmitted.
*
* @param timeout The timeout to wait for.
* @param unit The timeout time unit.
* @return A new message.
*
* @throws InvalidVersionException
* @throws IOException
* @throws TimeoutException
*/
public SignalServiceEnvelope read(long timeout, TimeUnit unit)
throws InvalidVersionException, IOException, TimeoutException
{
return read(timeout, unit, new NullMessagePipeCallback());
}
/**
* A blocking call that reads a message off the pipe (see {@link #read(long, java.util.concurrent.TimeUnit)}
*
* Unlike {@link #read(long, java.util.concurrent.TimeUnit)}, this method allows you
* to specify a callback that will be called before the received message is acknowledged.
* This allows you to write the received message to durable storage before acknowledging
* receipt of it to the server.
*
* @param timeout The timeout to wait for.
* @param unit The timeout time unit.
* @param callback A callback that will be called before the message receipt is
* acknowledged to the server.
* @return The message read (same as the message sent through the callback).
* @throws TimeoutException
* @throws IOException
* @throws InvalidVersionException
*/
public SignalServiceEnvelope read(long timeout, TimeUnit unit, MessagePipeCallback callback)
throws TimeoutException, IOException, InvalidVersionException
{
if (!credentialsProvider.isPresent()) {
throw new IllegalArgumentException("You can't read messages if you haven't specified credentials");
}
while (true) {
WebSocketRequestMessage request = websocket.readRequest(unit.toMillis(timeout));
WebSocketResponseMessage response = createWebSocketResponse(request);
boolean signalKeyEncrypted = isSignalKeyEncrypted(request);
try {
if (isSignalServiceEnvelope(request)) {
SignalServiceEnvelope envelope = new SignalServiceEnvelope(request.getBody().toByteArray(),
credentialsProvider.get().getSignalingKey(),
signalKeyEncrypted);
callback.onMessage(envelope);
return envelope;
}
} finally {
websocket.sendResponse(response);
}
}
}
public SendMessageResponse send(OutgoingPushMessageList list, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
try {
List<String> headers = new LinkedList<String>() {{
add("content-type:application/json");
}};
if (unidentifiedAccess.isPresent()) {
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
}
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(SecureRandom.getInstance("SHA1PRNG").nextLong())
.setVerb("PUT")
.setPath(String.format("/v1/messages/%s", list.getDestination()))
.addAllHeaders(headers)
.setBody(ByteString.copyFrom(JsonUtil.toJson(list).getBytes()))
.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
if (response.first() < 200 || response.first() >= 300) {
throw new IOException("Non-successful response: " + response.first());
}
if (Util.isEmpty(response.second())) return new SendMessageResponse(false);
else return JsonUtil.fromJson(response.second(), SendMessageResponse.class);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(e);
}
}
public SignalServiceProfile getProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
try {
List<String> headers = new LinkedList<>();
if (unidentifiedAccess.isPresent()) {
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
}
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(SecureRandom.getInstance("SHA1PRNG").nextLong())
.setVerb("GET")
.setPath(String.format("/v1/profile/%s", address.getIdentifier()))
.addAllHeaders(headers)
.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
if (response.first() < 200 || response.first() >= 300) {
throw new IOException("Non-successful response: " + response.first());
}
return JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError(nsae);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(e);
}
}
public AttachmentUploadAttributes getAttachmentUploadAttributes() throws IOException {
try {
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(new SecureRandom().nextLong())
.setVerb("GET")
.setPath("/v2/attachments/form/upload")
.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
if (response.first() < 200 || response.first() >= 300) {
throw new IOException("Non-successful response: " + response.first());
}
return JsonUtil.fromJson(response.second(), AttachmentUploadAttributes.class);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IOException(e);
}
}
/**
* Close this connection to the server.
*/
public void shutdown() {
websocket.disconnect();
}
private boolean isSignalServiceEnvelope(WebSocketRequestMessage message) {
return "PUT".equals(message.getVerb()) && "/api/v1/message".equals(message.getPath());
}
private boolean isSignalKeyEncrypted(WebSocketRequestMessage message) {
List<String> headers = message.getHeadersList();
if (headers == null || headers.isEmpty()) {
return true;
}
for (String header : headers) {
String[] parts = header.split(":");
if (parts.length == 2 && parts[0] != null && parts[0].trim().equalsIgnoreCase("X-Signal-Key")) {
if (parts[1] != null && parts[1].trim().equalsIgnoreCase("false")) {
return false;
}
}
}
return true;
}
private WebSocketResponseMessage createWebSocketResponse(WebSocketRequestMessage request) {
if (isSignalServiceEnvelope(request)) {
return WebSocketResponseMessage.newBuilder()
.setId(request.getId())
.setStatus(200)
.setMessage("OK")
.build();
} else {
return WebSocketResponseMessage.newBuilder()
.setId(request.getId())
.setStatus(400)
.setMessage("Unknown")
.build();
}
}
/**
* For receiving a callback when a new message has been
* received.
*/
public static interface MessagePipeCallback {
public void onMessage(SignalServiceEnvelope envelope);
}
private static class NullMessagePipeCallback implements MessagePipeCallback {
@Override
public void onMessage(SignalServiceEnvelope envelope) {}
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.websocket.ConnectivityListener;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.SignalServiceEnvelopeEntity;
import org.whispersystems.signalservice.internal.sticker.StickerProtos;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
/**
* The primary interface for receiving Signal Service messages.
*
* @author Moxie Marlinspike
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class SignalServiceMessageReceiver {
private final PushServiceSocket socket;
private final SignalServiceConfiguration urls;
private final CredentialsProvider credentialsProvider;
private final String userAgent;
private final ConnectivityListener connectivityListener;
private final SleepTimer sleepTimer;
/**
* Construct a SignalServiceMessageReceiver.
*
* @param urls The URL of the Signal Service.
* @param uuid The Signal Service UUID.
* @param e164 The Signal Service phone number.
* @param password The Signal Service user password.
* @param signalingKey The 52 byte signaling key assigned to this user at registration.
*/
public SignalServiceMessageReceiver(SignalServiceConfiguration urls,
UUID uuid, String e164, String password,
String signalingKey, String userAgent,
ConnectivityListener listener,
SleepTimer timer)
{
this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), userAgent, listener, timer);
}
/**
* Construct a SignalServiceMessageReceiver.
*
* @param urls The URL of the Signal Service.
* @param credentials The Signal Service user's credentials.
*/
public SignalServiceMessageReceiver(SignalServiceConfiguration urls,
CredentialsProvider credentials,
String userAgent,
ConnectivityListener listener,
SleepTimer timer)
{
this.urls = urls;
this.credentialsProvider = credentials;
this.socket = new PushServiceSocket(urls, credentials, userAgent);
this.userAgent = userAgent;
this.connectivityListener = listener;
this.sleepTimer = timer;
}
/**
* Retrieves a SignalServiceAttachment.
*
* @param pointer The {@link SignalServiceAttachmentPointer}
* received in a {@link SignalServiceDataMessage}.
* @param destination The download destination for this attachment.
*
* @return An InputStream that streams the plaintext attachment contents.
* @throws IOException
* @throws InvalidMessageException
*/
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes)
throws IOException, InvalidMessageException
{
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
}
public SignalServiceProfile retrieveProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
throws IOException
{
return socket.retrieveProfile(address, unidentifiedAccess);
}
public SignalServiceProfile retrieveProfileByUsername(String username, Optional<UnidentifiedAccess> unidentifiedAccess)
throws IOException
{
return socket.retrieveProfileByUsername(username, unidentifiedAccess);
}
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
throws IOException
{
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
return new ProfileCipherInputStream(new FileInputStream(destination), profileKey);
}
/**
* Retrieves a SignalServiceAttachment.
*
* @param pointer The {@link SignalServiceAttachmentPointer}
* received in a {@link SignalServiceDataMessage}.
* @param destination The download destination for this attachment.
* @param listener An optional listener (may be null) to receive callbacks on download progress.
*
* @return An InputStream that streams the plaintext attachment contents.
* @throws IOException
* @throws InvalidMessageException
*/
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener)
throws IOException, InvalidMessageException
{
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener);
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
}
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
throws IOException, InvalidMessageException
{
byte[] data = socket.retrieveSticker(packId, stickerId);
return AttachmentCipherInputStream.createForStickerData(data, packKey);
}
/**
* Retrieves a {@link SignalServiceStickerManifest}.
*
* @param packId The 16-byte packId that identifies the sticker pack.
* @param packKey The 32-byte packKey that decrypts the sticker pack.
* @return The {@link SignalServiceStickerManifest} representing the sticker pack.
* @throws IOException
* @throws InvalidMessageException
*/
public SignalServiceStickerManifest retrieveStickerManifest(byte[] packId, byte[] packKey)
throws IOException, InvalidMessageException
{
byte[] manifestBytes = socket.retrieveStickerManifest(packId);
InputStream cipherStream = AttachmentCipherInputStream.createForStickerData(manifestBytes, packKey);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Util.copy(cipherStream, outputStream);
StickerProtos.Pack pack = StickerProtos.Pack.parseFrom(outputStream.toByteArray());
List<SignalServiceStickerManifest.StickerInfo> stickers = new ArrayList<>(pack.getStickersCount());
SignalServiceStickerManifest.StickerInfo cover = pack.hasCover() ? new SignalServiceStickerManifest.StickerInfo(pack.getCover().getId(), pack.getCover().getEmoji())
: null;
for (StickerProtos.Pack.Sticker sticker : pack.getStickersList()) {
stickers.add(new SignalServiceStickerManifest.StickerInfo(sticker.getId(), sticker.getEmoji()));
}
return new SignalServiceStickerManifest(pack.getTitle(), pack.getAuthor(), cover, stickers);
}
/**
* Creates a pipe for receiving SignalService messages.
*
* Callers must call {@link SignalServiceMessagePipe#shutdown()} when finished with the pipe.
*
* @return A SignalServiceMessagePipe for receiving Signal Service messages.
*/
public SignalServiceMessagePipe createMessagePipe() {
WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(),
urls.getSignalServiceUrls()[0].getTrustStore(),
Optional.of(credentialsProvider), userAgent, connectivityListener,
sleepTimer);
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
}
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
WebSocketConnection webSocket = new WebSocketConnection(urls.getSignalServiceUrls()[0].getUrl(),
urls.getSignalServiceUrls()[0].getTrustStore(),
Optional.<CredentialsProvider>absent(), userAgent, connectivityListener,
sleepTimer);
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
}
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {
return retrieveMessages(new NullMessageReceivedCallback());
}
public List<SignalServiceEnvelope> retrieveMessages(MessageReceivedCallback callback)
throws IOException
{
List<SignalServiceEnvelope> results = new LinkedList<>();
List<SignalServiceEnvelopeEntity> entities = socket.getMessages();
for (SignalServiceEnvelopeEntity entity : entities) {
SignalServiceEnvelope envelope;
if (entity.hasSource() && entity.getSourceDevice() > 0) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(entity.getSourceUuid()), entity.getSourceE164());
envelope = new SignalServiceEnvelope(entity.getType(), Optional.of(address),
entity.getSourceDevice(), entity.getTimestamp(),
entity.getMessage(), entity.getContent(),
entity.getServerTimestamp(), entity.getServerUuid());
} else {
envelope = new SignalServiceEnvelope(entity.getType(), entity.getTimestamp(),
entity.getMessage(), entity.getContent(),
entity.getServerTimestamp(), entity.getServerUuid());
}
callback.onMessage(envelope);
results.add(envelope);
if (envelope.hasUuid()) socket.acknowledgeMessage(envelope.getUuid());
else socket.acknowledgeMessage(entity.getSourceE164(), entity.getTimestamp());
}
return results;
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
socket.setSoTimeoutMillis(soTimeoutMillis);
}
public interface MessageReceivedCallback {
public void onMessage(SignalServiceEnvelope envelope);
}
public static class NullMessageReceivedCallback implements MessageReceivedCallback {
@Override
public void onMessage(SignalServiceEnvelope envelope) {}
}
}

View File

@@ -0,0 +1,22 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
class TokenException extends Exception {
private final TokenResponse nextToken;
private final boolean canAutomaticallyRetry;
TokenException(TokenResponse nextToken, boolean canAutomaticallyRetry) {
this.nextToken = nextToken;
this.canAutomaticallyRetry = canAutomaticallyRetry;
}
public TokenResponse getToken() {
return nextToken;
}
public boolean isCanAutomaticallyRetry() {
return canAutomaticallyRetry;
}
}

View File

@@ -0,0 +1,271 @@
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.InvalidMacException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.signalservice.internal.util.ContentLengthInputStream;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
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 FilterInputStream {
private static final int BLOCK_SIZE = 16;
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 32;
private Cipher cipher;
private boolean done;
private long totalDataSize;
private long totalRead;
private byte[] overflowBuffer;
public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest)
throws InvalidMessageException, IOException
{
try {
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
if (digest == null) {
throw new InvalidMacException("Missing digest!");
}
try (FileInputStream fin = new FileInputStream(file)) {
verifyMac(fin, file.length(), mac, digest);
}
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
if (plaintextLength != 0) {
inputStream = new ContentLengthInputStream(inputStream, plaintextLength);
}
return inputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
public static InputStream createForStickerData(byte[] data, byte[] packKey)
throws InvalidMessageException, IOException
{
try {
byte[] combinedKeyMaterial = new HKDFv3().deriveSecrets(packKey, "Sticker Pack".getBytes(), 64);
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
if (data.length <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
try (InputStream inputStream = new ByteArrayInputStream(data)) {
verifyMac(inputStream, data.length, mac, null);
}
return new AttachmentCipherInputStream(new ByteArrayInputStream(data), parts[0], data.length - BLOCK_SIZE - mac.getMacLength());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
private AttachmentCipherInputStream(InputStream inputStream, byte[] cipherKey, long totalDataSize)
throws IOException
{
super(inputStream);
try {
byte[] iv = new byte[BLOCK_SIZE];
readFully(iv);
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
this.done = false;
this.totalRead = 0;
this.totalDataSize = totalDataSize;
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | 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;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public long skip(long byteCount) throws IOException {
long skipped = 0L;
while (skipped < byteCount) {
byte[] buf = new byte[Math.min(4096, (int)(byteCount-skipped))];
int read = read(buf);
skipped += read;
}
return skipped;
}
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try {
int flourish = cipher.doFinal(buffer, offset);
done = true;
return flourish;
} catch (IllegalBlockSizeException | BadPaddingException | ShortBufferException e) {
throw new IOException(e);
}
}
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
int readLength = 0;
if (null != overflowBuffer) {
if (overflowBuffer.length > length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
return length;
} else if (overflowBuffer.length == length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = null;
return length;
} else {
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
readLength += overflowBuffer.length;
offset += readLength;
length -= readLength;
overflowBuffer = null;
}
}
if (length + totalRead > totalDataSize)
length = (int)(totalDataSize - totalRead);
byte[] internalBuffer = new byte[length];
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
totalRead += read;
try {
int outputLen = cipher.getOutputSize(read);
if (outputLen <= length) {
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
return readLength;
}
byte[] transientBuffer = new byte[outputLen];
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
if (outputLen <= length) {
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
readLength += outputLen;
} else {
System.arraycopy(transientBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
readLength += length;
}
return readLength;
} catch (ShortBufferException e) {
throw new AssertionError(e);
}
}
private static void verifyMac(InputStream inputStream, long length, Mac mac, byte[] theirDigest)
throws InvalidMacException
{
try {
MessageDigest digest = MessageDigest.getInstance("SHA256");
int remainingData = Util.toIntExact(length) - mac.getMacLength();
byte[] buffer = new byte[4096];
while (remainingData > 0) {
int read = inputStream.read(buffer, 0, Math.min(buffer.length, remainingData));
mac.update(buffer, 0, read);
digest.update(buffer, 0, read);
remainingData -= read;
}
byte[] ourMac = mac.doFinal();
byte[] theirMac = new byte[mac.getMacLength()];
Util.readFully(inputStream, theirMac);
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new InvalidMacException("MAC doesn't match!");
}
byte[] ourDigest = digest.digest(theirMac);
if (theirDigest != null && !MessageDigest.isEqual(ourDigest, theirDigest)) {
throw new InvalidMacException("Digest doesn't match!");
}
} catch (IOException | ArithmeticException e1) {
throw new InvalidMacException(e1);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
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

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
public class AttachmentCipherOutputStream extends DigestingOutputStream {
private final Cipher cipher;
private final Mac mac;
public AttachmentCipherOutputStream(byte[] combinedKeyMaterial,
OutputStream outputStream)
throws IOException
{
super(outputStream);
try {
this.cipher = initializeCipher();
this.mac = initializeMac();
byte[][] keyParts = Util.split(combinedKeyMaterial, 32, 32);
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyParts[0], "AES"));
this.mac.init(new SecretKeySpec(keyParts[1], "HmacSHA256"));
mac.update(cipher.getIV());
super.write(cipher.getIV());
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
byte[] ciphertext = cipher.update(buffer, offset, length);
if (ciphertext != null) {
mac.update(ciphertext);
super.write(ciphertext);
}
}
@Override
public void write(int b) {
throw new AssertionError("NYI");
}
@Override
public void flush() throws IOException {
try {
byte[] ciphertext = cipher.doFinal();
byte[] auth = mac.doFinal(ciphertext);
super.write(ciphertext);
super.write(auth);
super.flush();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public static long getCiphertextLength(long plaintextLength) {
return 16 + (((plaintextLength / 16) +1) * 16) + 32;
}
private Mac initializeMac() {
try {
return Mac.getInstance("HmacSHA256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private Cipher initializeCipher() {
try {
return Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,22 @@
package org.whispersystems.signalservice.api.crypto;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class CryptoUtil {
private CryptoUtil () { }
public static byte[] computeHmacSha256(byte[] key, byte[] data) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,55 @@
package org.whispersystems.signalservice.api.crypto;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public abstract class DigestingOutputStream extends FilterOutputStream {
private final MessageDigest runningDigest;
private byte[] digest;
public DigestingOutputStream(OutputStream outputStream) {
super(outputStream);
try {
this.runningDigest = MessageDigest.getInstance("SHA256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
runningDigest.update(buffer, 0, buffer.length);
out.write(buffer, 0, buffer.length);
}
public void write(byte[] buffer, int offset, int length) throws IOException {
runningDigest.update(buffer, offset, length);
out.write(buffer, offset, length);
}
public void write(int b) throws IOException {
runningDigest.update((byte)b);
out.write(b);
}
public void flush() throws IOException {
digest = runningDigest.digest();
out.flush();
}
public void close() throws IOException {
out.close();
}
public byte[] getTransmittedDigest() {
return digest;
}
}

View File

@@ -0,0 +1,11 @@
package org.whispersystems.signalservice.api.crypto;
public class InvalidCiphertextException extends Exception {
public InvalidCiphertextException(Exception nested) {
super(nested);
}
public InvalidCiphertextException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,101 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ProfileCipher {
public static final int NAME_PADDED_LENGTH = 26;
private final byte[] key;
public ProfileCipher(byte[] key) {
this.key = key;
}
public byte[] encryptName(byte[] input, int paddedLength) {
try {
byte[] inputPadded = new byte[paddedLength];
if (input.length > inputPadded.length) {
throw new IllegalArgumentException("Input is too long: " + new String(input));
}
System.arraycopy(input, 0, inputPadded, 0, input.length);
byte[] nonce = Util.getSecretBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
return ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public byte[] decryptName(byte[] input) throws InvalidCiphertextException {
try {
if (input.length < 12 + 16 + 1) {
throw new InvalidCiphertextException("Too short: " + input.length);
}
byte[] nonce = new byte[12];
System.arraycopy(input, 0, nonce, 0, nonce.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
byte[] paddedPlaintext = cipher.doFinal(input, nonce.length, input.length - nonce.length);
int plaintextLength = 0;
for (int i=paddedPlaintext.length-1;i>=0;i--) {
if (paddedPlaintext[i] != (byte)0x00) {
plaintextLength = i + 1;
break;
}
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(paddedPlaintext, 0, plaintext, 0, plaintextLength);
return plaintext;
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (InvalidKeyException | BadPaddingException e) {
throw new InvalidCiphertextException(e);
}
}
public boolean verifyUnidentifiedAccess(byte[] theirUnidentifiedAccessVerifier) {
try {
if (theirUnidentifiedAccessVerifier == null || theirUnidentifiedAccessVerifier.length == 0) return false;
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(key);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(unidentifiedAccessKey, "HmacSHA256"));
byte[] ourUnidentifiedAccessVerifier = mac.doFinal(new byte[32]);
return MessageDigest.isEqual(theirUnidentifiedAccessVerifier, ourUnidentifiedAccessVerifier);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,83 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ProfileCipherInputStream extends FilterInputStream {
private final Cipher cipher;
private boolean finished = false;
public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException {
super(in);
try {
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = new byte[12];
Util.readFully(in, nonce);
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
@Override
public int read() {
throw new AssertionError("Not supported!");
}
@Override
public int read(byte[] input) throws IOException {
return read(input, 0, input.length);
}
@Override
public int read(byte[] output, int outputOffset, int outputLength) throws IOException {
if (finished) return -1;
try {
byte[] ciphertext = new byte[outputLength / 2];
int read = in.read(ciphertext, 0, ciphertext.length);
if (read == -1) {
if (cipher.getOutputSize(0) > outputLength) {
throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength);
}
finished = true;
return cipher.doFinal(output, outputOffset);
} else {
if (cipher.getOutputSize(read) > outputLength) {
throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength);
}
return cipher.update(ciphertext, 0, read, output, outputOffset);
}
} catch (IllegalBlockSizeException | ShortBufferException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,78 @@
package org.whispersystems.signalservice.api.crypto;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ProfileCipherOutputStream extends DigestingOutputStream {
private final Cipher cipher;
public ProfileCipherOutputStream(OutputStream out, byte[] key) throws IOException {
super(out);
try {
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = generateNonce();
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
super.write(nonce, 0, nonce.length);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
byte[] output = cipher.update(buffer, offset, length);
super.write(output);
}
@Override
public void write(int b) throws IOException {
byte[] input = new byte[1];
input[0] = (byte)b;
byte[] output = cipher.update(input);
super.write(output);
}
@Override
public void flush() throws IOException {
try {
byte[] output = cipher.doFinal();
super.write(output);
super.flush();
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
}
}
private byte[] generateNonce() {
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
return nonce;
}
public static long getCiphertextLength(long plaintextLength) {
return 12 + 16 + plaintextLength;
}
}

View File

@@ -0,0 +1,861 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
import org.signal.libsignal.metadata.ProtocolInvalidVersionException;
import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SealedSessionCipher;
import org.signal.libsignal.metadata.SealedSessionCipher.DecryptionResult;
import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.SessionCipher;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
import org.whispersystems.libsignal.protocol.SignalMessage;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Sticker;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage.VerifiedState;
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.PushTransportDetails;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.util.Base64;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext.Type.DELIVER;
/**
* This is used to decrypt received {@link SignalServiceEnvelope}s.
*
* @author Moxie Marlinspike
*/
public class SignalServiceCipher {
@SuppressWarnings("unused")
private static final String TAG = SignalServiceCipher.class.getSimpleName();
private final SignalProtocolStore signalProtocolStore;
private final SignalServiceAddress localAddress;
private final CertificateValidator certificateValidator;
public SignalServiceCipher(SignalServiceAddress localAddress,
SignalProtocolStore signalProtocolStore,
CertificateValidator certificateValidator)
{
this.signalProtocolStore = signalProtocolStore;
this.localAddress = localAddress;
this.certificateValidator = certificateValidator;
}
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess,
byte[] unpaddedMessage)
throws UntrustedIdentityException, InvalidKeyException
{
if (unidentifiedAccess.isPresent()) {
SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1);
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
String body = Base64.encodeBytes(ciphertext);
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
} else {
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination);
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
String body = Base64.encodeBytes(message.serialize());
int type;
switch (message.getType()) {
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
default: throw new AssertionError("Bad type: " + message.getType());
}
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
}
}
/**
* Decrypt a received {@link SignalServiceEnvelope}
*
* @param envelope The received SignalServiceEnvelope
*
* @return a decrypted SignalServiceContent
*/
public SignalServiceContent decrypt(SignalServiceEnvelope envelope)
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
ProtocolInvalidKeyIdException, ProtocolLegacyMessageException,
ProtocolUntrustedIdentityException, ProtocolNoSessionException,
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
ProtocolInvalidKeyException, ProtocolDuplicateMessageException,
SelfSendException, UnsupportedDataMessageException
{
try {
if (envelope.hasLegacyMessage()) {
Plaintext plaintext = decrypt(envelope, envelope.getLegacyMessage());
DataMessage message = DataMessage.parseFrom(plaintext.getData());
return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (envelope.hasContent()) {
Plaintext plaintext = decrypt(envelope, envelope.getContent());
Content message = Content.parseFrom(plaintext.getData());
if (message.hasDataMessage()) {
return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message.getDataMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasSyncMessage() && localAddress.matches(plaintext.getMetadata().getSender())) {
return new SignalServiceContent(createSynchronizeMessage(plaintext.getMetadata(), message.getSyncMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasCallMessage()) {
return new SignalServiceContent(createCallMessage(message.getCallMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasReceiptMessage()) {
return new SignalServiceContent(createReceiptMessage(plaintext.getMetadata(), message.getReceiptMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
plaintext.getMetadata().isNeedsReceipt());
} else if (message.hasTypingMessage()) {
return new SignalServiceContent(createTypingMessage(plaintext.getMetadata(), message.getTypingMessage()),
plaintext.getMetadata().getSender(),
plaintext.getMetadata().getSenderDevice(),
plaintext.getMetadata().getTimestamp(),
false);
}
}
return null;
} catch (InvalidProtocolBufferException e) {
throw new InvalidMetadataMessageException(e);
}
}
private Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext)
throws InvalidMetadataMessageException, InvalidMetadataVersionException,
ProtocolDuplicateMessageException, ProtocolUntrustedIdentityException,
ProtocolLegacyMessageException, ProtocolInvalidKeyException,
ProtocolInvalidVersionException, ProtocolInvalidMessageException,
ProtocolInvalidKeyIdException, ProtocolNoSessionException,
SelfSendException
{
try {
byte[] paddedMessage;
Metadata metadata;
int sessionVersion;
if (!envelope.hasSource() && !envelope.isUnidentifiedSender()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Non-UD envelope is missing a source!"), null, 0);
}
if (envelope.isPreKeySignalMessage()) {
SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice());
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress);
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isSignalMessage()) {
SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice());
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress);
paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext));
metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isUnidentifiedSender()) {
SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1);
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp());
SignalServiceAddress resultAddress = new SignalServiceAddress(UuidUtil.parse(result.getSenderUuid().orNull()), result.getSenderE164());
SignalProtocolAddress protocolAddress = getPreferredProtocolAddress(signalProtocolStore, resultAddress, result.getDeviceId());
paddedMessage = result.getPaddedMessage();
metadata = new Metadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), true);
sessionVersion = sealedSessionCipher.getSessionVersion(protocolAddress);
} else {
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
}
PushTransportDetails transportDetails = new PushTransportDetails(sessionVersion);
byte[] data = transportDetails.getStrippedPaddingMessageBody(paddedMessage);
return new Plaintext(metadata, data);
} catch (DuplicateMessageException e) {
throw new ProtocolDuplicateMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (LegacyMessageException e) {
throw new ProtocolLegacyMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidMessageException e) {
throw new ProtocolInvalidMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidKeyIdException e) {
throw new ProtocolInvalidKeyIdException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidKeyException e) {
throw new ProtocolInvalidKeyException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (UntrustedIdentityException e) {
throw new ProtocolUntrustedIdentityException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (InvalidVersionException e) {
throw new ProtocolInvalidVersionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
} catch (NoSessionException e) {
throw new ProtocolNoSessionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice());
}
}
private static SignalProtocolAddress getPreferredProtocolAddress(SignalProtocolStore store, SignalServiceAddress address, int sourceDevice) {
SignalProtocolAddress uuidAddress = address.getUuid().isPresent() ? new SignalProtocolAddress(address.getUuid().get().toString(), sourceDevice) : null;
SignalProtocolAddress e164Address = address.getNumber().isPresent() ? new SignalProtocolAddress(address.getNumber().get(), sourceDevice) : null;
if (uuidAddress != null && store.containsSession(uuidAddress)) {
return uuidAddress;
} else if (e164Address != null && store.containsSession(e164Address)) {
return e164Address;
} else {
return new SignalProtocolAddress(address.getLegacyIdentifier(), sourceDevice);
}
}
private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content)
throws ProtocolInvalidMessageException, UnsupportedDataMessageException
{
SignalServiceGroup groupInfo = createGroupInfo(content);
List<SignalServiceAttachment> attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & DataMessage.Flags.END_SESSION_VALUE ) != 0);
boolean expirationUpdate = ((content.getFlags() & DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
boolean profileKeyUpdate = ((content.getFlags() & DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0);
SignalServiceDataMessage.Quote quote = createQuote(content);
List<SharedContact> sharedContacts = createSharedContacts(content);
List<Preview> previews = createPreviews(content);
Sticker sticker = createSticker(content);
Reaction reaction = createReaction(content);
if (content.getRequiredProtocolVersion() > DataMessage.ProtocolVersion.CURRENT.getNumber()) {
throw new UnsupportedDataMessageException(DataMessage.ProtocolVersion.CURRENT.getNumber(),
content.getRequiredProtocolVersion(),
metadata.getSender().getIdentifier(),
metadata.getSenderDevice(),
Optional.fromNullable(groupInfo));
}
for (AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(createAttachmentPointer(pointer));
}
if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()),
metadata.getSender().getIdentifier(),
metadata.getSenderDevice());
}
return new SignalServiceDataMessage(metadata.getTimestamp(),
groupInfo,
attachments,
content.getBody(),
endSession,
content.getExpireTimer(),
expirationUpdate,
content.hasProfileKey() ? content.getProfileKey().toByteArray() : null,
profileKeyUpdate,
quote,
sharedContacts,
previews,
sticker,
content.getIsViewOnce(),
reaction);
}
private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content)
throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException
{
if (content.hasSent()) {
SyncMessage.Sent sentContent = content.getSent();
SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage());
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid(), sentContent.getDestinationE164())
? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
: Optional.<SignalServiceAddress>absent();
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0);
}
for (SyncMessage.Sent.UnidentifiedDeliveryStatus status : sentContent.getUnidentifiedStatusList()) {
if (SignalServiceAddress.isValidAddress(status.getDestinationUuid(), status.getDestinationE164())) {
SignalServiceAddress recipient = new SignalServiceAddress(UuidUtil.parseOrNull(status.getDestinationUuid()), status.getDestinationE164());
unidentifiedStatuses.put(recipient, status.getUnidentified());
} else {
Log.w(TAG, "Encountered an invalid UnidentifiedDeliveryStatus in a SentTranscript! Ignoring.");
}
}
return SignalServiceSyncMessage.forSentTranscript(new SentTranscriptMessage(address,
sentContent.getTimestamp(),
dataMessage,
sentContent.getExpirationStartTimestamp(),
unidentifiedStatuses,
sentContent.getIsRecipientUpdate()));
}
if (content.hasRequest()) {
return SignalServiceSyncMessage.forRequest(new RequestMessage(content.getRequest()));
}
if (content.getReadList().size() > 0) {
List<ReadMessage> readMessages = new LinkedList<>();
for (SyncMessage.Read read : content.getReadList()) {
if (SignalServiceAddress.isValidAddress(read.getSenderUuid(), read.getSenderE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(read.getSenderUuid()), read.getSenderE164());
readMessages.add(new ReadMessage(address, read.getTimestamp()));
} else {
Log.w(TAG, "Encountered an invalid ReadMessage! Ignoring.");
}
}
return SignalServiceSyncMessage.forRead(readMessages);
}
if (content.hasViewOnceOpen()) {
if (SignalServiceAddress.isValidAddress(content.getViewOnceOpen().getSenderUuid(), content.getViewOnceOpen().getSenderE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getViewOnceOpen().getSenderUuid()), content.getViewOnceOpen().getSenderE164());
ViewOnceOpenMessage timerRead = new ViewOnceOpenMessage(address, content.getViewOnceOpen().getTimestamp());
return SignalServiceSyncMessage.forViewOnceOpen(timerRead);
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("ViewOnceOpen message has no sender!"), null, 0);
}
}
if (content.hasVerified()) {
if (SignalServiceAddress.isValidAddress(content.getVerified().getDestinationUuid(), content.getVerified().getDestinationE164())) {
try {
Verified verified = content.getVerified();
SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(verified.getDestinationUuid()), verified.getDestinationE164());
IdentityKey identityKey = new IdentityKey(verified.getIdentityKey().toByteArray(), 0);
VerifiedState verifiedState;
if (verified.getState() == Verified.State.DEFAULT) {
verifiedState = VerifiedState.DEFAULT;
} else if (verified.getState() == Verified.State.VERIFIED) {
verifiedState = VerifiedState.VERIFIED;
} else if (verified.getState() == Verified.State.UNVERIFIED) {
verifiedState = VerifiedState.UNVERIFIED;
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Unknown state: " + verified.getState().getNumber()),
metadata.getSender().getIdentifier(), metadata.getSenderDevice());
}
return SignalServiceSyncMessage.forVerified(new VerifiedMessage(destination, identityKey, verifiedState, System.currentTimeMillis()));
} catch (InvalidKeyException e) {
throw new ProtocolInvalidKeyException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice());
}
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Verified message has no sender!"), null, 0);
}
}
if (content.getStickerPackOperationList().size() > 0) {
List<StickerPackOperationMessage> operations = new LinkedList<>();
for (SyncMessage.StickerPackOperation operation : content.getStickerPackOperationList()) {
byte[] packId = operation.hasPackId() ? operation.getPackId().toByteArray() : null;
byte[] packKey = operation.hasPackKey() ? operation.getPackKey().toByteArray() : null;
StickerPackOperationMessage.Type type = null;
if (operation.hasType()) {
switch (operation.getType()) {
case INSTALL: type = StickerPackOperationMessage.Type.INSTALL; break;
case REMOVE: type = StickerPackOperationMessage.Type.REMOVE; break;
}
}
operations.add(new StickerPackOperationMessage(packId, packKey, type));
}
return SignalServiceSyncMessage.forStickerPackOperations(operations);
}
if (content.hasBlocked()) {
List<String> numbers = content.getBlocked().getNumbersList();
List<String> uuids = content.getBlocked().getUuidsList();
List<SignalServiceAddress> addresses = new ArrayList<>(numbers.size() + uuids.size());
List<byte[]> groupIds = new ArrayList<>(content.getBlocked().getGroupIdsList().size());
for (String e164 : numbers) {
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(null, e164);
if (address.isPresent()) {
addresses.add(address.get());
}
}
for (String uuid : uuids) {
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(uuid, null);
if (address.isPresent()) {
addresses.add(address.get());
}
}
for (ByteString groupId : content.getBlocked().getGroupIdsList()) {
groupIds.add(groupId.toByteArray());
}
return SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds));
}
if (content.hasConfiguration()) {
Boolean readReceipts = content.getConfiguration().hasReadReceipts() ? content.getConfiguration().getReadReceipts() : null;
Boolean unidentifiedDeliveryIndicators = content.getConfiguration().hasUnidentifiedDeliveryIndicators() ? content.getConfiguration().getUnidentifiedDeliveryIndicators() : null;
Boolean typingIndicators = content.getConfiguration().hasTypingIndicators() ? content.getConfiguration().getTypingIndicators() : null;
Boolean linkPreviews = content.getConfiguration().hasLinkPreviews() ? content.getConfiguration().getLinkPreviews() : null;
return SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.fromNullable(readReceipts),
Optional.fromNullable(unidentifiedDeliveryIndicators),
Optional.fromNullable(typingIndicators),
Optional.fromNullable(linkPreviews)));
}
if (content.hasFetchLatest() && content.getFetchLatest().hasType()) {
switch (content.getFetchLatest().getType()) {
case LOCAL_PROFILE: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE);
case STORAGE_MANIFEST: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST);
}
}
return SignalServiceSyncMessage.empty();
}
private SignalServiceCallMessage createCallMessage(CallMessage content) {
if (content.hasOffer()) {
CallMessage.Offer offerContent = content.getOffer();
return SignalServiceCallMessage.forOffer(new OfferMessage(offerContent.getId(), offerContent.getDescription()));
} else if (content.hasAnswer()) {
CallMessage.Answer answerContent = content.getAnswer();
return SignalServiceCallMessage.forAnswer(new AnswerMessage(answerContent.getId(), answerContent.getDescription()));
} else if (content.getIceUpdateCount() > 0) {
List<IceUpdateMessage> iceUpdates = new LinkedList<>();
for (CallMessage.IceUpdate iceUpdate : content.getIceUpdateList()) {
iceUpdates.add(new IceUpdateMessage(iceUpdate.getId(), iceUpdate.getSdpMid(), iceUpdate.getSdpMLineIndex(), iceUpdate.getSdp()));
}
return SignalServiceCallMessage.forIceUpdates(iceUpdates);
} else if (content.hasHangup()) {
CallMessage.Hangup hangup = content.getHangup();
return SignalServiceCallMessage.forHangup(new HangupMessage(hangup.getId()));
} else if (content.hasBusy()) {
CallMessage.Busy busy = content.getBusy();
return SignalServiceCallMessage.forBusy(new BusyMessage(busy.getId()));
}
return SignalServiceCallMessage.empty();
}
private SignalServiceReceiptMessage createReceiptMessage(Metadata metadata, ReceiptMessage content) {
SignalServiceReceiptMessage.Type type;
if (content.getType() == ReceiptMessage.Type.DELIVERY) type = SignalServiceReceiptMessage.Type.DELIVERY;
else if (content.getType() == ReceiptMessage.Type.READ) type = SignalServiceReceiptMessage.Type.READ;
else type = SignalServiceReceiptMessage.Type.UNKNOWN;
return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp());
}
private SignalServiceTypingMessage createTypingMessage(Metadata metadata, TypingMessage content) throws ProtocolInvalidMessageException {
SignalServiceTypingMessage.Action action;
if (content.getAction() == TypingMessage.Action.STARTED) action = SignalServiceTypingMessage.Action.STARTED;
else if (content.getAction() == TypingMessage.Action.STOPPED) action = SignalServiceTypingMessage.Action.STOPPED;
else action = SignalServiceTypingMessage.Action.UNKNOWN;
if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()),
metadata.getSender().getIdentifier(),
metadata.getSenderDevice());
}
return new SignalServiceTypingMessage(action, content.getTimestamp(),
content.hasGroupId() ? Optional.of(content.getGroupId().toByteArray()) :
Optional.<byte[]>absent());
}
private SignalServiceDataMessage.Quote createQuote(DataMessage content) {
if (!content.hasQuote()) return null;
List<SignalServiceDataMessage.Quote.QuotedAttachment> attachments = new LinkedList<>();
for (DataMessage.Quote.QuotedAttachment attachment : content.getQuote().getAttachmentsList()) {
attachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
attachment.getFileName(),
attachment.hasThumbnail() ? createAttachmentPointer(attachment.getThumbnail()) : null));
}
if (SignalServiceAddress.isValidAddress(content.getQuote().getAuthorUuid(), content.getQuote().getAuthorE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getQuote().getAuthorUuid()), content.getQuote().getAuthorE164());
return new SignalServiceDataMessage.Quote(content.getQuote().getId(),
address,
content.getQuote().getText(),
attachments);
} else {
Log.w(TAG, "Quote was missing an author! Returning null.");
return null;
}
}
private List<Preview> createPreviews(DataMessage content) {
if (content.getPreviewCount() <= 0) return null;
List<Preview> results = new LinkedList<>();
for (DataMessage.Preview preview : content.getPreviewList()) {
SignalServiceAttachment attachment = null;
if (preview.hasImage()) {
attachment = createAttachmentPointer(preview.getImage());
}
results.add(new Preview(preview.getUrl(),
preview.getTitle(),
Optional.fromNullable(attachment)));
}
return results;
}
private Sticker createSticker(DataMessage content) {
if (!content.hasSticker() ||
!content.getSticker().hasPackId() ||
!content.getSticker().hasPackKey() ||
!content.getSticker().hasStickerId() ||
!content.getSticker().hasData())
{
return null;
}
DataMessage.Sticker sticker = content.getSticker();
return new Sticker(sticker.getPackId().toByteArray(),
sticker.getPackKey().toByteArray(),
sticker.getStickerId(),
createAttachmentPointer(sticker.getData()));
}
private Reaction createReaction(DataMessage content) {
if (!content.hasReaction() ||
!content.getReaction().hasEmoji() ||
!(content.getReaction().hasTargetAuthorE164() || content.getReaction().hasTargetAuthorUuid()) ||
!content.getReaction().hasTargetSentTimestamp())
{
return null;
}
DataMessage.Reaction reaction = content.getReaction();
return new Reaction(reaction.getEmoji(),
reaction.getRemove(),
new SignalServiceAddress(UuidUtil.parseOrNull(reaction.getTargetAuthorUuid()), reaction.getTargetAuthorE164()),
reaction.getTargetSentTimestamp());
}
private List<SharedContact> createSharedContacts(DataMessage content) {
if (content.getContactCount() <= 0) return null;
List<SharedContact> results = new LinkedList<>();
for (DataMessage.Contact contact : content.getContactList()) {
SharedContact.Builder builder = SharedContact.newBuilder()
.setName(SharedContact.Name.newBuilder()
.setDisplay(contact.getName().getDisplayName())
.setFamily(contact.getName().getFamilyName())
.setGiven(contact.getName().getGivenName())
.setMiddle(contact.getName().getMiddleName())
.setPrefix(contact.getName().getPrefix())
.setSuffix(contact.getName().getSuffix())
.build());
if (contact.getAddressCount() > 0) {
for (DataMessage.Contact.PostalAddress address : contact.getAddressList()) {
SharedContact.PostalAddress.Type type = SharedContact.PostalAddress.Type.HOME;
switch (address.getType()) {
case WORK: type = SharedContact.PostalAddress.Type.WORK; break;
case HOME: type = SharedContact.PostalAddress.Type.HOME; break;
case CUSTOM: type = SharedContact.PostalAddress.Type.CUSTOM; break;
}
builder.withAddress(SharedContact.PostalAddress.newBuilder()
.setCity(address.getCity())
.setCountry(address.getCountry())
.setLabel(address.getLabel())
.setNeighborhood(address.getNeighborhood())
.setPobox(address.getPobox())
.setPostcode(address.getPostcode())
.setRegion(address.getRegion())
.setStreet(address.getStreet())
.setType(type)
.build());
}
}
if (contact.getNumberCount() > 0) {
for (DataMessage.Contact.Phone phone : contact.getNumberList()) {
SharedContact.Phone.Type type = SharedContact.Phone.Type.HOME;
switch (phone.getType()) {
case HOME: type = SharedContact.Phone.Type.HOME; break;
case WORK: type = SharedContact.Phone.Type.WORK; break;
case MOBILE: type = SharedContact.Phone.Type.MOBILE; break;
case CUSTOM: type = SharedContact.Phone.Type.CUSTOM; break;
}
builder.withPhone(SharedContact.Phone.newBuilder()
.setLabel(phone.getLabel())
.setType(type)
.setValue(phone.getValue())
.build());
}
}
if (contact.getEmailCount() > 0) {
for (DataMessage.Contact.Email email : contact.getEmailList()) {
SharedContact.Email.Type type = SharedContact.Email.Type.HOME;
switch (email.getType()) {
case HOME: type = SharedContact.Email.Type.HOME; break;
case WORK: type = SharedContact.Email.Type.WORK; break;
case MOBILE: type = SharedContact.Email.Type.MOBILE; break;
case CUSTOM: type = SharedContact.Email.Type.CUSTOM; break;
}
builder.withEmail(SharedContact.Email.newBuilder()
.setLabel(email.getLabel())
.setType(type)
.setValue(email.getValue())
.build());
}
}
if (contact.hasAvatar()) {
builder.setAvatar(SharedContact.Avatar.newBuilder()
.withAttachment(createAttachmentPointer(contact.getAvatar().getAvatar()))
.withProfileFlag(contact.getAvatar().getIsProfile())
.build());
}
if (contact.hasOrganization()) {
builder.withOrganization(contact.getOrganization());
}
results.add(builder.build());
}
return results;
}
private SignalServiceAttachmentPointer createAttachmentPointer(AttachmentPointer pointer) {
return new SignalServiceAttachmentPointer(pointer.getId(),
pointer.getContentType(),
pointer.getKey().toByteArray(),
pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.<Integer>absent(),
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.<byte[]>absent(),
pointer.getWidth(), pointer.getHeight(),
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.<byte[]>absent(),
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>absent(),
(pointer.getFlags() & AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0,
pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.<String>absent(),
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>absent());
}
private SignalServiceGroup createGroupInfo(DataMessage content) throws ProtocolInvalidMessageException {
if (!content.hasGroup()) return null;
SignalServiceGroup.Type type;
switch (content.getGroup().getType()) {
case DELIVER: type = SignalServiceGroup.Type.DELIVER; break;
case UPDATE: type = SignalServiceGroup.Type.UPDATE; break;
case QUIT: type = SignalServiceGroup.Type.QUIT; break;
case REQUEST_INFO: type = SignalServiceGroup.Type.REQUEST_INFO; break;
default: type = SignalServiceGroup.Type.UNKNOWN; break;
}
if (content.getGroup().getType() != DELIVER) {
String name = null;
List<SignalServiceAddress> members = null;
SignalServiceAttachmentPointer avatar = null;
if (content.getGroup().hasName()) {
name = content.getGroup().getName();
}
if (content.getGroup().getMembersCount() > 0) {
members = new ArrayList<>(content.getGroup().getMembersCount());
for (SignalServiceProtos.GroupContext.Member member : content.getGroup().getMembersList()) {
if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) {
members.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164()));
} else {
throw new ProtocolInvalidMessageException(new InvalidMessageException("GroupContext.Member had no address!"), null, 0);
}
}
} else if (content.getGroup().getMembersE164Count() > 0) {
members = new ArrayList<>(content.getGroup().getMembersE164Count());
for (String member : content.getGroup().getMembersE164List()) {
members.add(new SignalServiceAddress(null, member));
}
}
if (content.getGroup().hasAvatar()) {
AttachmentPointer pointer = content.getGroup().getAvatar();
avatar = new SignalServiceAttachmentPointer(pointer.getId(),
pointer.getContentType(),
pointer.getKey().toByteArray(),
Optional.of(pointer.getSize()),
Optional.<byte[]>absent(), 0, 0,
Optional.fromNullable(pointer.hasDigest() ? pointer.getDigest().toByteArray() : null),
Optional.<String>absent(),
false,
Optional.<String>absent(),
Optional.<String>absent());
}
return new SignalServiceGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
}
return new SignalServiceGroup(content.getGroup().getId().toByteArray());
}
private static class Metadata {
private final SignalServiceAddress sender;
private final int senderDevice;
private final long timestamp;
private final boolean needsReceipt;
private Metadata(SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
}
public SignalServiceAddress getSender() {
return sender;
}
public int getSenderDevice() {
return senderDevice;
}
public long getTimestamp() {
return timestamp;
}
public boolean isNeedsReceipt() {
return needsReceipt;
}
}
private static class Plaintext {
private final Metadata metadata;
private final byte[] data;
private Plaintext(Metadata metadata, byte[] data) {
this.metadata = metadata;
this.data = data;
}
public Metadata getMetadata() {
return metadata;
}
public byte[] getData() {
return data;
}
}
}

View File

@@ -0,0 +1,54 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class UnidentifiedAccess {
private final byte[] unidentifiedAccessKey;
private final SenderCertificate unidentifiedCertificate;
public UnidentifiedAccess(byte[] unidentifiedAccessKey, byte[] unidentifiedCertificate)
throws InvalidCertificateException
{
this.unidentifiedAccessKey = unidentifiedAccessKey;
this.unidentifiedCertificate = new SenderCertificate(unidentifiedCertificate);
}
public byte[] getUnidentifiedAccessKey() {
return unidentifiedAccessKey;
}
public SenderCertificate getUnidentifiedCertificate() {
return unidentifiedCertificate;
}
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
try {
byte[] nonce = new byte[12];
byte[] input = new byte[16];
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
byte[] ciphertext = cipher.doFinal(input);
return ByteUtil.trim(ciphertext, 16);
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,23 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.util.guava.Optional;
public class UnidentifiedAccessPair {
private final Optional<UnidentifiedAccess> targetUnidentifiedAccess;
private final Optional<UnidentifiedAccess> selfUnidentifiedAccess;
public UnidentifiedAccessPair(UnidentifiedAccess targetUnidentifiedAccess, UnidentifiedAccess selfUnidentifiedAccess) {
this.targetUnidentifiedAccess = Optional.of(targetUnidentifiedAccess);
this.selfUnidentifiedAccess = Optional.of(selfUnidentifiedAccess);
}
public Optional<UnidentifiedAccess> getTargetUnidentifiedAccess() {
return targetUnidentifiedAccess;
}
public Optional<UnidentifiedAccess> getSelfUnidentifiedAccess() {
return selfUnidentifiedAccess;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.IdentityKey;
public class UntrustedIdentityException extends Exception {
private final IdentityKey identityKey;
private final String identifier;
public UntrustedIdentityException(String s, String identifier, IdentityKey identityKey) {
super(s);
this.identifier = identifier;
this.identityKey = identityKey;
}
public UntrustedIdentityException(UntrustedIdentityException e) {
this(e.getMessage(), e.getIdentifier(), e.getIdentityKey());
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public String getIdentifier() {
return identifier;
}
}

View File

@@ -0,0 +1,91 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SendMessageResult {
private final SignalServiceAddress address;
private final Success success;
private final boolean networkFailure;
private final boolean unregisteredFailure;
private final IdentityFailure identityFailure;
public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync) {
return new SendMessageResult(address, new Success(unidentified, needsSync), false, false, null);
}
public static SendMessageResult networkFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, true, false, null);
}
public static SendMessageResult unregisteredFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, false, true, null);
}
public static SendMessageResult identityFailure(SignalServiceAddress address, IdentityKey identityKey) {
return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey));
}
public SignalServiceAddress getAddress() {
return address;
}
public Success getSuccess() {
return success;
}
public boolean isNetworkFailure() {
return networkFailure;
}
public boolean isUnregisteredFailure() {
return unregisteredFailure;
}
public IdentityFailure getIdentityFailure() {
return identityFailure;
}
private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure) {
this.address = address;
this.success = success;
this.networkFailure = networkFailure;
this.unregisteredFailure = unregisteredFailure;
this.identityFailure = identityFailure;
}
public static class Success {
private final boolean unidentified;
private final boolean needsSync;
private Success(boolean unidentified, boolean needsSync) {
this.unidentified = unidentified;
this.needsSync = needsSync;
}
public boolean isUnidentified() {
return unidentified;
}
public boolean isNeedsSync() {
return needsSync;
}
}
public static class IdentityFailure {
private final IdentityKey identityKey;
private IdentityFailure(IdentityKey identityKey) {
this.identityKey = identityKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.InputStream;
public abstract class SignalServiceAttachment {
private final String contentType;
protected SignalServiceAttachment(String contentType) {
this.contentType = contentType;
}
public String getContentType() {
return contentType;
}
public abstract boolean isStream();
public abstract boolean isPointer();
public SignalServiceAttachmentStream asStream() {
return (SignalServiceAttachmentStream)this;
}
public SignalServiceAttachmentPointer asPointer() {
return (SignalServiceAttachmentPointer)this;
}
public static Builder newStreamBuilder() {
return new Builder();
}
public static class Builder {
private InputStream inputStream;
private String contentType;
private String fileName;
private long length;
private ProgressListener listener;
private boolean voiceNote;
private int width;
private int height;
private String caption;
private String blurHash;
private Builder() {}
public Builder withStream(InputStream inputStream) {
this.inputStream = inputStream;
return this;
}
public Builder withContentType(String contentType) {
this.contentType = contentType;
return this;
}
public Builder withLength(long length) {
this.length = length;
return this;
}
public Builder withFileName(String fileName) {
this.fileName = fileName;
return this;
}
public Builder withListener(ProgressListener listener) {
this.listener = listener;
return this;
}
public Builder withVoiceNote(boolean voiceNote) {
this.voiceNote = voiceNote;
return this;
}
public Builder withWidth(int width) {
this.width = width;
return this;
}
public Builder withHeight(int height) {
this.height = height;
return this;
}
public Builder withCaption(String caption) {
this.caption = caption;
return this;
}
public Builder withBlurHash(String blurHash) {
this.blurHash = blurHash;
return this;
}
public SignalServiceAttachmentStream build() {
if (inputStream == null) throw new IllegalArgumentException("Must specify stream!");
if (contentType == null) throw new IllegalArgumentException("No content type specified!");
if (length == 0) throw new IllegalArgumentException("No length specified!");
return new SignalServiceAttachmentStream(inputStream,
contentType,
length,
Optional.fromNullable(fileName),
voiceNote,
Optional.<byte[]>absent(),
width,
height,
Optional.fromNullable(caption),
Optional.fromNullable(blurHash),
listener);
}
}
/**
* An interface to receive progress information on upload/download of
* an attachment.
*/
public interface ProgressListener {
/**
* Called on a progress change event.
*
* @param total The total amount to transmit/receive in bytes.
* @param progress The amount that has been transmitted/received in bytes thus far
*/
public void onAttachmentProgress(long total, long progress);
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
/**
* Represents a received SignalServiceAttachment "handle." This
* is a pointer to the actual attachment content, which needs to be
* retrieved using {@link SignalServiceMessageReceiver#retrieveAttachment(SignalServiceAttachmentPointer, java.io.File, int)}
*
* @author Moxie Marlinspike
*/
public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
private final long id;
private final byte[] key;
private final Optional<Integer> size;
private final Optional<byte[]> preview;
private final Optional<byte[]> digest;
private final Optional<String> fileName;
private final boolean voiceNote;
private final int width;
private final int height;
private final Optional<String> caption;
private final Optional<String> blurHash;
public SignalServiceAttachmentPointer(long id, String contentType, byte[] key,
Optional<Integer> size, Optional<byte[]> preview,
int width, int height,
Optional<byte[]> digest, Optional<String> fileName,
boolean voiceNote, Optional<String> caption,
Optional<String> blurHash)
{
super(contentType);
this.id = id;
this.key = key;
this.size = size;
this.preview = preview;
this.width = width;
this.height = height;
this.digest = digest;
this.fileName = fileName;
this.voiceNote = voiceNote;
this.caption = caption;
this.blurHash = blurHash;
}
public long getId() {
return id;
}
public byte[] getKey() {
return key;
}
@Override
public boolean isStream() {
return false;
}
@Override
public boolean isPointer() {
return true;
}
public Optional<Integer> getSize() {
return size;
}
public Optional<String> getFileName() {
return fileName;
}
public Optional<byte[]> getPreview() {
return preview;
}
public Optional<byte[]> getDigest() {
return digest;
}
public boolean getVoiceNote() {
return voiceNote;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Optional<String> getCaption() {
return caption;
}
public Optional<String> getBlurHash() {
return blurHash;
}
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.InputStream;
/**
* Represents a local SignalServiceAttachment to be sent.
*/
public class SignalServiceAttachmentStream extends SignalServiceAttachment {
private final InputStream inputStream;
private final long length;
private final Optional<String> fileName;
private final ProgressListener listener;
private final Optional<byte[]> preview;
private final boolean voiceNote;
private final int width;
private final int height;
private final Optional<String> caption;
private final Optional<String> blurHash;
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, ProgressListener listener) {
this(inputStream, contentType, length, fileName, voiceNote, Optional.<byte[]>absent(), 0, 0, Optional.<String>absent(), Optional.<String>absent(), listener);
}
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, Optional<byte[]> preview, int width, int height, Optional<String> caption, Optional<String> blurHash, ProgressListener listener) {
super(contentType);
this.inputStream = inputStream;
this.length = length;
this.fileName = fileName;
this.listener = listener;
this.voiceNote = voiceNote;
this.preview = preview;
this.width = width;
this.height = height;
this.caption = caption;
this.blurHash = blurHash;
}
@Override
public boolean isStream() {
return true;
}
@Override
public boolean isPointer() {
return false;
}
public InputStream getInputStream() {
return inputStream;
}
public long getLength() {
return length;
}
public Optional<String> getFileName() {
return fileName;
}
public ProgressListener getListener() {
return listener;
}
public Optional<byte[]> getPreview() {
return preview;
}
public boolean getVoiceNote() {
return voiceNote;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Optional<String> getCaption() {
return caption;
}
public Optional<String> getBlurHash() {
return blurHash;
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SignalServiceContent {
private final SignalServiceAddress sender;
private final int senderDevice;
private final long timestamp;
private final boolean needsReceipt;
private final Optional<SignalServiceDataMessage> message;
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
private final Optional<SignalServiceCallMessage> callMessage;
private final Optional<SignalServiceReceiptMessage> readMessage;
private final Optional<SignalServiceTypingMessage> typingMessage;
public SignalServiceContent(SignalServiceDataMessage message, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.fromNullable(message);
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.fromNullable(synchronizeMessage);
this.callMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceCallMessage callMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.of(callMessage);
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceReceiptMessage receiptMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.readMessage = Optional.of(receiptMessage);
this.typingMessage = Optional.absent();
}
public SignalServiceContent(SignalServiceTypingMessage typingMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = needsReceipt;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.of(typingMessage);
}
public Optional<SignalServiceDataMessage> getDataMessage() {
return message;
}
public Optional<SignalServiceSyncMessage> getSyncMessage() {
return synchronizeMessage;
}
public Optional<SignalServiceCallMessage> getCallMessage() {
return callMessage;
}
public Optional<SignalServiceReceiptMessage> getReceiptMessage() {
return readMessage;
}
public Optional<SignalServiceTypingMessage> getTypingMessage() {
return typingMessage;
}
public SignalServiceAddress getSender() {
return sender;
}
public int getSenderDevice() {
return senderDevice;
}
public long getTimestamp() {
return timestamp;
}
public boolean isNeedsReceipt() {
return needsReceipt;
}
}

View File

@@ -0,0 +1,501 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.LinkedList;
import java.util.List;
/**
* Represents a decrypted Signal Service data message.
*/
public class SignalServiceDataMessage {
private final long timestamp;
private final Optional<List<SignalServiceAttachment>> attachments;
private final Optional<String> body;
private final Optional<SignalServiceGroup> group;
private final Optional<byte[]> profileKey;
private final boolean endSession;
private final boolean expirationUpdate;
private final int expiresInSeconds;
private final boolean profileKeyUpdate;
private final Optional<Quote> quote;
private final Optional<List<SharedContact>> contacts;
private final Optional<List<Preview>> previews;
private final Optional<Sticker> sticker;
private final boolean viewOnce;
private final Optional<Reaction> reaction;
/**
* Construct a SignalServiceDataMessage with a body and no attachments.
*
* @param timestamp The sent timestamp.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, String body) {
this(timestamp, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage with a body and no attachments.
*
* @param timestamp The sent timestamp.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
*/
public SignalServiceDataMessage(long timestamp, String body, int expiresInSeconds) {
this(timestamp, (List<SignalServiceAttachment>)null, body, expiresInSeconds);
}
public SignalServiceDataMessage(final long timestamp, final SignalServiceAttachment attachment, final String body) {
this(timestamp, new LinkedList<SignalServiceAttachment>() {{add(attachment);}}, body);
}
/**
* Construct a SignalServiceDataMessage with a body and list of attachments.
*
* @param timestamp The sent timestamp.
* @param attachments The attachments.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body) {
this(timestamp, attachments, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage with a body and list of attachments.
*
* @param timestamp The sent timestamp.
* @param attachments The attachments.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
*/
public SignalServiceDataMessage(long timestamp, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
this(timestamp, null, attachments, body, expiresInSeconds);
}
/**
* Construct a SignalServiceDataMessage group message with attachments and body.
*
* @param timestamp The sent timestamp.
* @param group The group information.
* @param attachments The attachments.
* @param body The message contents.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body) {
this(timestamp, group, attachments, body, 0);
}
/**
* Construct an expiring SignalServiceDataMessage group message with attachments and body.
*
* @param timestamp The sent timestamp.
* @param group The group information.
* @param attachments The attachments.
* @param body The message contents.
* @param expiresInSeconds The number of seconds in which a message should disappear after having been seen.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List<SignalServiceAttachment> attachments, String body, int expiresInSeconds) {
this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null, false, null);
}
/**
* Construct a SignalServiceDataMessage.
*
* @param timestamp The sent timestamp.
* @param group The group information (or null if none).
* @param attachments The attachments (or null if none).
* @param body The message contents.
* @param endSession Flag indicating whether this message should close a session.
* @param expiresInSeconds Number of seconds in which the message should disappear after being seen.
*/
public SignalServiceDataMessage(long timestamp, SignalServiceGroup group,
List<SignalServiceAttachment> attachments,
String body, boolean endSession, int expiresInSeconds,
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker, boolean viewOnce, Reaction reaction)
{
this.timestamp = timestamp;
this.body = Optional.fromNullable(body);
this.group = Optional.fromNullable(group);
this.endSession = endSession;
this.expiresInSeconds = expiresInSeconds;
this.expirationUpdate = expirationUpdate;
this.profileKey = Optional.fromNullable(profileKey);
this.profileKeyUpdate = profileKeyUpdate;
this.quote = Optional.fromNullable(quote);
this.sticker = Optional.fromNullable(sticker);
this.viewOnce = viewOnce;
this.reaction = Optional.fromNullable(reaction);
if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments);
} else {
this.attachments = Optional.absent();
}
if (sharedContacts != null && !sharedContacts.isEmpty()) {
this.contacts = Optional.of(sharedContacts);
} else {
this.contacts = Optional.absent();
}
if (previews != null && !previews.isEmpty()) {
this.previews = Optional.of(previews);
} else {
this.previews = Optional.absent();
}
}
public static Builder newBuilder() {
return new Builder();
}
/**
* @return The message timestamp.
*/
public long getTimestamp() {
return timestamp;
}
/**
* @return The message attachments (if any).
*/
public Optional<List<SignalServiceAttachment>> getAttachments() {
return attachments;
}
/**
* @return The message body (if any).
*/
public Optional<String> getBody() {
return body;
}
/**
* @return The message group info (if any).
*/
public Optional<SignalServiceGroup> getGroupInfo() {
return group;
}
public boolean isEndSession() {
return endSession;
}
public boolean isExpirationUpdate() {
return expirationUpdate;
}
public boolean isProfileKeyUpdate() {
return profileKeyUpdate;
}
public boolean isGroupUpdate() {
return group.isPresent() && group.get().getType() != SignalServiceGroup.Type.DELIVER;
}
public int getExpiresInSeconds() {
return expiresInSeconds;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public Optional<Quote> getQuote() {
return quote;
}
public Optional<List<SharedContact>> getSharedContacts() {
return contacts;
}
public Optional<List<Preview>> getPreviews() {
return previews;
}
public Optional<Sticker> getSticker() {
return sticker;
}
public boolean isViewOnce() {
return viewOnce;
}
public Optional<Reaction> getReaction() {
return reaction;
}
public static class Builder {
private List<SignalServiceAttachment> attachments = new LinkedList<>();
private List<SharedContact> sharedContacts = new LinkedList<>();
private List<Preview> previews = new LinkedList<>();
private long timestamp;
private SignalServiceGroup group;
private String body;
private boolean endSession;
private int expiresInSeconds;
private boolean expirationUpdate;
private byte[] profileKey;
private boolean profileKeyUpdate;
private Quote quote;
private Sticker sticker;
private boolean viewOnce;
private Reaction reaction;
private Builder() {}
public Builder withTimestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
public Builder asGroupMessage(SignalServiceGroup group) {
this.group = group;
return this;
}
public Builder withAttachment(SignalServiceAttachment attachment) {
this.attachments.add(attachment);
return this;
}
public Builder withAttachments(List<SignalServiceAttachment> attachments) {
this.attachments.addAll(attachments);
return this;
}
public Builder withBody(String body) {
this.body = body;
return this;
}
public Builder asEndSessionMessage() {
return asEndSessionMessage(true);
}
public Builder asEndSessionMessage(boolean endSession) {
this.endSession = endSession;
return this;
}
public Builder asExpirationUpdate() {
return asExpirationUpdate(true);
}
public Builder asExpirationUpdate(boolean expirationUpdate) {
this.expirationUpdate = expirationUpdate;
return this;
}
public Builder withExpiration(int expiresInSeconds) {
this.expiresInSeconds = expiresInSeconds;
return this;
}
public Builder withProfileKey(byte[] profileKey) {
this.profileKey = profileKey;
return this;
}
public Builder asProfileKeyUpdate(boolean profileKeyUpdate) {
this.profileKeyUpdate = profileKeyUpdate;
return this;
}
public Builder withQuote(Quote quote) {
this.quote = quote;
return this;
}
public Builder withSharedContact(SharedContact contact) {
this.sharedContacts.add(contact);
return this;
}
public Builder withSharedContacts(List<SharedContact> contacts) {
this.sharedContacts.addAll(contacts);
return this;
}
public Builder withPreviews(List<Preview> previews) {
this.previews.addAll(previews);
return this;
}
public Builder withSticker(Sticker sticker) {
this.sticker = sticker;
return this;
}
public Builder withViewOnce(boolean viewOnce) {
this.viewOnce = viewOnce;
return this;
}
public Builder withReaction(Reaction reaction) {
this.reaction = reaction;
return this;
}
public SignalServiceDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis();
return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession,
expiresInSeconds, expirationUpdate, profileKey,
profileKeyUpdate, quote, sharedContacts, previews,
sticker, viewOnce, reaction);
}
}
public static class Quote {
private final long id;
private final SignalServiceAddress author;
private final String text;
private final List<QuotedAttachment> attachments;
public Quote(long id, SignalServiceAddress author, String text, List<QuotedAttachment> attachments) {
this.id = id;
this.author = author;
this.text = text;
this.attachments = attachments;
}
public long getId() {
return id;
}
public SignalServiceAddress getAuthor() {
return author;
}
public String getText() {
return text;
}
public List<QuotedAttachment> getAttachments() {
return attachments;
}
public static class QuotedAttachment {
private final String contentType;
private final String fileName;
private final SignalServiceAttachment thumbnail;
public QuotedAttachment(String contentType, String fileName, SignalServiceAttachment thumbnail) {
this.contentType = contentType;
this.fileName = fileName;
this.thumbnail = thumbnail;
}
public String getContentType() {
return contentType;
}
public String getFileName() {
return fileName;
}
public SignalServiceAttachment getThumbnail() {
return thumbnail;
}
}
}
public static class Preview {
private final String url;
private final String title;
private final Optional<SignalServiceAttachment> image;
public Preview(String url, String title, Optional<SignalServiceAttachment> image) {
this.url = url;
this.title = title;
this.image = image;
}
public String getUrl() {
return url;
}
public String getTitle() {
return title;
}
public Optional<SignalServiceAttachment> getImage() {
return image;
}
}
public static class Sticker {
private final byte[] packId;
private final byte[] packKey;
private final int stickerId;
private final SignalServiceAttachment attachment;
public Sticker(byte[] packId, byte[] packKey, int stickerId, SignalServiceAttachment attachment) {
this.packId = packId;
this.packKey = packKey;
this.stickerId = stickerId;
this.attachment = attachment;
}
public byte[] getPackId() {
return packId;
}
public byte[] getPackKey() {
return packKey;
}
public int getStickerId() {
return stickerId;
}
public SignalServiceAttachment getAttachment() {
return attachment;
}
}
public static class Reaction {
private final String emoji;
private final boolean remove;
private final SignalServiceAddress targetAuthor;
private final long targetSentTimestamp;
public Reaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, long targetSentTimestamp) {
this.emoji = emoji;
this.remove = remove;
this.targetAuthor = targetAuthor;
this.targetSentTimestamp = targetSentTimestamp;
}
public String getEmoji() {
return emoji;
}
public boolean isRemove() {
return remove;
}
public SignalServiceAddress getTargetAuthor() {
return targetAuthor;
}
public long getTargetSentTimestamp() {
return targetSentTimestamp;
}
}
}

View File

@@ -0,0 +1,331 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* This class represents an encrypted Signal Service envelope.
*
* The envelope contains the wrapping information, such as the sender, the
* message timestamp, the encrypted message type, etc.
*
* @author Moxie Marlinspike
*/
public class SignalServiceEnvelope {
private static final String TAG = SignalServiceEnvelope.class.getSimpleName();
private static final int SUPPORTED_VERSION = 1;
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10;
private static final int VERSION_OFFSET = 0;
private static final int VERSION_LENGTH = 1;
private static final int IV_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
private static final int IV_LENGTH = 16;
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH;
private final Envelope envelope;
/**
* Construct an envelope from a serialized, Base64 encoded SignalServiceEnvelope, encrypted
* with a signaling key.
*
* @param message The serialized SignalServiceEnvelope, base64 encoded and encrypted.
* @param signalingKey The signaling key.
* @throws IOException
* @throws InvalidVersionException
*/
public SignalServiceEnvelope(String message, String signalingKey, boolean isSignalingKeyEncrypted)
throws IOException, InvalidVersionException
{
this(Base64.decode(message), signalingKey, isSignalingKeyEncrypted);
}
/**
* Construct an envelope from a serialized SignalServiceEnvelope, encrypted with a signaling key.
*
* @param input The serialized and (optionally) encrypted SignalServiceEnvelope.
* @param signalingKey The signaling key.
* @throws InvalidVersionException
* @throws IOException
*/
public SignalServiceEnvelope(byte[] input, String signalingKey, boolean isSignalingKeyEncrypted)
throws InvalidVersionException, IOException
{
if (!isSignalingKeyEncrypted) {
this.envelope = Envelope.parseFrom(input);
} else {
if (input.length < VERSION_LENGTH || input[VERSION_OFFSET] != SUPPORTED_VERSION) {
throw new InvalidVersionException("Unsupported version!");
}
SecretKeySpec cipherKey = getCipherKey(signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey);
verifyMac(input, macKey);
this.envelope = Envelope.parseFrom(getPlaintext(input, cipherKey));
}
}
public SignalServiceEnvelope(int type, Optional<SignalServiceAddress> sender, int senderDevice, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) {
Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(type))
.setSourceDevice(senderDevice)
.setTimestamp(timestamp)
.setServerTimestamp(serverTimestamp);
if (sender.isPresent()) {
if (sender.get().getUuid().isPresent()) {
builder.setSourceUuid(sender.get().getUuid().get().toString());
}
if (sender.get().getNumber().isPresent()) {
builder.setSourceE164(sender.get().getNumber().get());
}
}
if (uuid != null) {
builder.setServerGuid(uuid);
}
if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage));
if (content != null) builder.setContent(ByteString.copyFrom(content));
this.envelope = builder.build();
}
public SignalServiceEnvelope(int type, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) {
Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(type))
.setTimestamp(timestamp)
.setServerTimestamp(serverTimestamp);
if (uuid != null) {
builder.setServerGuid(uuid);
}
if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage));
if (content != null) builder.setContent(ByteString.copyFrom(content));
this.envelope = builder.build();
}
public String getUuid() {
return envelope.getServerGuid();
}
public boolean hasUuid() {
return envelope.hasServerGuid();
}
/**
* @return True if either a source E164 or UUID is present.
*/
public boolean hasSource() {
return envelope.hasSourceE164() || envelope.hasSourceUuid();
}
/**
* @return The envelope's sender as an E164 number.
*/
public Optional<String> getSourceE164() {
return Optional.fromNullable(envelope.getSourceE164());
}
/**
* @return The envelope's sender as a UUID.
*/
public Optional<String> getSourceUuid() {
return Optional.fromNullable(envelope.getSourceUuid());
}
public String getSourceIdentifier() {
return getSourceUuid().or(getSourceE164()).orNull();
}
public boolean hasSourceDevice() {
return envelope.hasSourceDevice();
}
/**
* @return The envelope's sender device ID.
*/
public int getSourceDevice() {
return envelope.getSourceDevice();
}
/**
* @return The envelope's sender as a SignalServiceAddress.
*/
public SignalServiceAddress getSourceAddress() {
return new SignalServiceAddress(UuidUtil.parseOrNull(envelope.getSourceUuid()), envelope.getSourceE164());
}
/**
* @return The envelope content type.
*/
public int getType() {
return envelope.getType().getNumber();
}
/**
* @return The timestamp this envelope was sent.
*/
public long getTimestamp() {
return envelope.getTimestamp();
}
public long getServerTimestamp() {
return envelope.getServerTimestamp();
}
/**
* @return Whether the envelope contains a SignalServiceDataMessage
*/
public boolean hasLegacyMessage() {
return envelope.hasLegacyMessage();
}
/**
* @return The envelope's containing SignalService message.
*/
public byte[] getLegacyMessage() {
return envelope.getLegacyMessage().toByteArray();
}
/**
* @return Whether the envelope contains an encrypted SignalServiceContent
*/
public boolean hasContent() {
return envelope.hasContent();
}
/**
* @return The envelope's encrypted SignalServiceContent.
*/
public byte[] getContent() {
return envelope.getContent().toByteArray();
}
/**
* @return true if the containing message is a {@link org.whispersystems.libsignal.protocol.SignalMessage}
*/
public boolean isSignalMessage() {
return envelope.getType().getNumber() == Envelope.Type.CIPHERTEXT_VALUE;
}
/**
* @return true if the containing message is a {@link org.whispersystems.libsignal.protocol.PreKeySignalMessage}
*/
public boolean isPreKeySignalMessage() {
return envelope.getType().getNumber() == Envelope.Type.PREKEY_BUNDLE_VALUE;
}
/**
* @return true if the containing message is a delivery receipt.
*/
public boolean isReceipt() {
return envelope.getType().getNumber() == Envelope.Type.RECEIPT_VALUE;
}
public boolean isUnidentifiedSender() {
return envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
}
private byte[] getPlaintext(byte[] ciphertext, SecretKeySpec cipherKey) throws IOException {
try {
byte[] ivBytes = new byte[IV_LENGTH];
System.arraycopy(ciphertext, IV_OFFSET, ivBytes, 0, ivBytes.length);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
return cipher.doFinal(ciphertext, CIPHERTEXT_OFFSET,
ciphertext.length - VERSION_LENGTH - IV_LENGTH - MAC_SIZE);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (BadPaddingException e) {
Log.w(TAG, e);
throw new IOException("Bad padding?");
}
}
private void verifyMac(byte[] ciphertext, SecretKeySpec macKey) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(macKey);
if (ciphertext.length < MAC_SIZE + 1)
throw new IOException("Invalid MAC!");
mac.update(ciphertext, 0, ciphertext.length - MAC_SIZE);
byte[] ourMacFull = mac.doFinal();
byte[] ourMacBytes = new byte[MAC_SIZE];
System.arraycopy(ourMacFull, 0, ourMacBytes, 0, ourMacBytes.length);
byte[] theirMacBytes = new byte[MAC_SIZE];
System.arraycopy(ciphertext, ciphertext.length-MAC_SIZE, theirMacBytes, 0, theirMacBytes.length);
Log.w(TAG, "Our MAC: " + Hex.toString(ourMacBytes));
Log.w(TAG, "Thr MAC: " + Hex.toString(theirMacBytes));
if (!Arrays.equals(ourMacBytes, theirMacBytes)) {
throw new IOException("Invalid MAC compare!");
}
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES");
}
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] macKey = new byte[MAC_KEY_SIZE];
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256");
}
}

View File

@@ -0,0 +1,143 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
/**
* Group information to include in SignalServiceMessages destined to groups.
*
* This class represents a "context" that is included with Signal Service messages
* to make them group messages. There are three types of context:
*
* 1) Update -- Sent when either creating a group, or updating the properties
* of a group (such as the avatar icon, membership list, or title).
* 2) Deliver -- Sent when a message is to be delivered to an existing group.
* 3) Quit -- Sent when the sender wishes to leave an existing group.
*
* @author Moxie Marlinspike
*/
public class SignalServiceGroup {
public enum Type {
UNKNOWN,
UPDATE,
DELIVER,
QUIT,
REQUEST_INFO
}
private final byte[] groupId;
private final Type type;
private final Optional<String> name;
private final Optional<List<SignalServiceAddress>> members;
private final Optional<SignalServiceAttachment> avatar;
/**
* Construct a DELIVER group context.
* @param groupId
*/
public SignalServiceGroup(byte[] groupId) {
this(Type.DELIVER, groupId, null, null, null);
}
/**
* Construct a group context.
* @param type The group message type (update, deliver, quit).
* @param groupId The group ID.
* @param name The group title.
* @param members The group membership list.
* @param avatar The group avatar icon.
*/
public SignalServiceGroup(Type type, byte[] groupId, String name,
List<SignalServiceAddress> members,
SignalServiceAttachment avatar)
{
this.type = type;
this.groupId = groupId;
this.name = Optional.fromNullable(name);
this.members = Optional.fromNullable(members);
this.avatar = Optional.fromNullable(avatar);
}
public byte[] getGroupId() {
return groupId;
}
public Type getType() {
return type;
}
public Optional<String> getName() {
return name;
}
public Optional<List<SignalServiceAddress>> getMembers() {
return members;
}
public Optional<SignalServiceAttachment> getAvatar() {
return avatar;
}
public static Builder newUpdateBuilder() {
return new Builder(Type.UPDATE);
}
public static Builder newBuilder(Type type) {
return new Builder(type);
}
public static class Builder {
private Type type;
private byte[] id;
private String name;
private List<SignalServiceAddress> members;
private SignalServiceAttachment avatar;
private Builder(Type type) {
this.type = type;
}
public Builder withId(byte[] id) {
this.id = id;
return this;
}
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder withMembers(List<SignalServiceAddress> members) {
this.members = members;
return this;
}
public Builder withAvatar(SignalServiceAttachment avatar) {
this.avatar = avatar;
return this;
}
public SignalServiceGroup build() {
if (id == null) throw new IllegalArgumentException("No group ID specified!");
if (type == Type.UPDATE && name == null && members == null && avatar == null) {
throw new IllegalArgumentException("Group update with no updates!");
}
return new SignalServiceGroup(type, id, name, members, avatar);
}
}
}

View File

@@ -0,0 +1,41 @@
package org.whispersystems.signalservice.api.messages;
import java.util.List;
public class SignalServiceReceiptMessage {
public enum Type {
UNKNOWN, DELIVERY, READ
}
private final Type type;
private final List<Long> timestamps;
private final long when;
public SignalServiceReceiptMessage(Type type, List<Long> timestamps, long when) {
this.type = type;
this.timestamps = timestamps;
this.when = when;
}
public Type getType() {
return type;
}
public List<Long> getTimestamps() {
return timestamps;
}
public long getWhen() {
return when;
}
public boolean isDeliveryReceipt() {
return type == Type.DELIVERY;
}
public boolean isReadReceipt() {
return type == Type.READ;
}
}

View File

@@ -0,0 +1,56 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SignalServiceStickerManifest {
private final Optional<String> title;
private final Optional<String> author;
private final Optional<StickerInfo> cover;
private final List<StickerInfo> stickers;
public SignalServiceStickerManifest(String title, String author, StickerInfo cover, List<StickerInfo> stickers) {
this.title = Optional.of(title);
this.author = Optional.of(author);
this.cover = Optional.of(cover);
this.stickers = (stickers == null) ? Collections.<StickerInfo>emptyList() : new ArrayList<>(stickers);
}
public Optional<String> getTitle() {
return title;
}
public Optional<String> getAuthor() {
return author;
}
public Optional<StickerInfo> getCover() {
return cover;
}
public List<StickerInfo> getStickers() {
return stickers;
}
public static final class StickerInfo {
private final int id;
private final String emoji;
public StickerInfo(int id, String emoji) {
this.id = id;
this.emoji = emoji;
}
public int getId() {
return id;
}
public String getEmoji() {
return emoji;
}
}
}

View File

@@ -0,0 +1,40 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.util.guava.Optional;
public class SignalServiceTypingMessage {
public enum Action {
UNKNOWN, STARTED, STOPPED
}
private final Action action;
private final long timestamp;
private final Optional<byte[]> groupId;
public SignalServiceTypingMessage(Action action, long timestamp, Optional<byte[]> groupId) {
this.action = action;
this.timestamp = timestamp;
this.groupId = groupId;
}
public Action getAction() {
return action;
}
public long getTimestamp() {
return timestamp;
}
public Optional<byte[]> getGroupId() {
return groupId;
}
public boolean isTypingStarted() {
return action == Action.STARTED;
}
public boolean isTypingStopped() {
return action == Action.STOPPED;
}
}

View File

@@ -0,0 +1,21 @@
package org.whispersystems.signalservice.api.messages.calls;
public class AnswerMessage {
private final long id;
private final String description;
public AnswerMessage(long id, String description) {
this.id = id;
this.description = description;
}
public String getDescription() {
return description;
}
public long getId() {
return id;
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.signalservice.api.messages.calls;
public class BusyMessage {
private final long id;
public BusyMessage(long id) {
this.id = id;
}
public long getId() {
return id;
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.signalservice.api.messages.calls;
public class HangupMessage {
private final long id;
public HangupMessage(long id) {
this.id = id;
}
public long getId() {
return id;
}
}

View File

@@ -0,0 +1,33 @@
package org.whispersystems.signalservice.api.messages.calls;
public class IceUpdateMessage {
private final long id;
private final String sdpMid;
private final int sdpMLineIndex;
private final String sdp;
public IceUpdateMessage(long id, String sdpMid, int sdpMLineIndex, String sdp) {
this.id = id;
this.sdpMid = sdpMid;
this.sdpMLineIndex = sdpMLineIndex;
this.sdp = sdp;
}
public String getSdpMid() {
return sdpMid;
}
public int getSdpMLineIndex() {
return sdpMLineIndex;
}
public String getSdp() {
return sdp;
}
public long getId() {
return id;
}
}

View File

@@ -0,0 +1,21 @@
package org.whispersystems.signalservice.api.messages.calls;
public class OfferMessage {
private final long id;
private final String description;
public OfferMessage(long id, String description) {
this.id = id;
this.description = description;
}
public String getDescription() {
return description;
}
public long getId() {
return id;
}
}

View File

@@ -0,0 +1,108 @@
package org.whispersystems.signalservice.api.messages.calls;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.LinkedList;
import java.util.List;
public class SignalServiceCallMessage {
private final Optional<OfferMessage> offerMessage;
private final Optional<AnswerMessage> answerMessage;
private final Optional<HangupMessage> hangupMessage;
private final Optional<BusyMessage> busyMessage;
private final Optional<List<IceUpdateMessage>> iceUpdateMessages;
private SignalServiceCallMessage(Optional<OfferMessage> offerMessage,
Optional<AnswerMessage> answerMessage,
Optional<List<IceUpdateMessage>> iceUpdateMessages,
Optional<HangupMessage> hangupMessage,
Optional<BusyMessage> busyMessage)
{
this.offerMessage = offerMessage;
this.answerMessage = answerMessage;
this.iceUpdateMessages = iceUpdateMessages;
this.hangupMessage = hangupMessage;
this.busyMessage = busyMessage;
}
public static SignalServiceCallMessage forOffer(OfferMessage offerMessage) {
return new SignalServiceCallMessage(Optional.of(offerMessage),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forAnswer(AnswerMessage answerMessage) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.of(answerMessage),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forIceUpdates(List<IceUpdateMessage> iceUpdateMessages) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.of(iceUpdateMessages),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forIceUpdate(final IceUpdateMessage iceUpdateMessage) {
List<IceUpdateMessage> iceUpdateMessages = new LinkedList<>();
iceUpdateMessages.add(iceUpdateMessage);
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.of(iceUpdateMessages),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forHangup(HangupMessage hangupMessage) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.of(hangupMessage),
Optional.<BusyMessage>absent());
}
public static SignalServiceCallMessage forBusy(BusyMessage busyMessage) {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.of(busyMessage));
}
public static SignalServiceCallMessage empty() {
return new SignalServiceCallMessage(Optional.<OfferMessage>absent(),
Optional.<AnswerMessage>absent(),
Optional.<List<IceUpdateMessage>>absent(),
Optional.<HangupMessage>absent(),
Optional.<BusyMessage>absent());
}
public Optional<List<IceUpdateMessage>> getIceUpdateMessages() {
return iceUpdateMessages;
}
public Optional<AnswerMessage> getAnswerMessage() {
return answerMessage;
}
public Optional<OfferMessage> getOfferMessage() {
return offerMessage;
}
public Optional<HangupMessage> getHangupMessage() {
return hangupMessage;
}
public Optional<BusyMessage> getBusyMessage() {
return busyMessage;
}
}

View File

@@ -0,0 +1,30 @@
package org.whispersystems.signalservice.api.messages.calls;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class TurnServerInfo {
@JsonProperty
private String username;
@JsonProperty
private String password;
@JsonProperty
private List<String> urls;
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public List<String> getUrls() {
return urls;
}
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class BlockedListMessage {
private final List<SignalServiceAddress> addresses;
private final List<byte[]> groupIds;
public BlockedListMessage(List<SignalServiceAddress> addresses, List<byte[]> groupIds) {
this.addresses = addresses;
this.groupIds = groupIds;
}
public List<SignalServiceAddress> getAddresses() {
return addresses;
}
public List<byte[]> getGroupIds() {
return groupIds;
}
}

View File

@@ -0,0 +1,116 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ChunkedInputStream {
protected final InputStream in;
public ChunkedInputStream(InputStream in) {
this.in = in;
}
protected int readRawVarint32() throws IOException {
byte tmp = (byte)in.read();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7f;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if ((tmp = (byte)in.read()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
result |= (tmp = (byte)in.read()) << 28;
if (tmp < 0) {
// Discard upper 32 bits.
for (int i = 0; i < 5; i++) {
if ((byte)in.read() >= 0) {
return result;
}
}
throw new IOException("Malformed varint!");
}
}
}
}
return result;
}
protected static final class LimitedInputStream extends FilterInputStream {
private long left;
private long mark = -1;
LimitedInputStream(InputStream in, long limit) {
super(in);
left = limit;
}
@Override public int available() throws IOException {
return (int) Math.min(in.available(), left);
}
// it's okay to mark even if mark isn't supported, as reset won't work
@Override public synchronized void mark(int readLimit) {
in.mark(readLimit);
mark = left;
}
@Override public int read() throws IOException {
if (left == 0) {
return -1;
}
int result = in.read();
if (result != -1) {
--left;
}
return result;
}
@Override public int read(byte[] b, int off, int len) throws IOException {
if (left == 0) {
return -1;
}
len = (int) Math.min(len, left);
int result = in.read(b, off, len);
if (result != -1) {
left -= result;
}
return result;
}
@Override public synchronized void reset() throws IOException {
if (!in.markSupported()) {
throw new IOException("Mark not supported");
}
if (mark == -1) {
throw new IOException("Mark not set");
}
in.reset();
left = mark;
}
@Override public long skip(long n) throws IOException {
n = Math.min(n, left);
long skipped = in.skip(n);
left -= skipped;
return skipped;
}
}
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ChunkedOutputStream {
protected final OutputStream out;
public ChunkedOutputStream(OutputStream out) {
this.out = out;
}
protected void writeVarint32(int value) throws IOException {
while (true) {
if ((value & ~0x7F) == 0) {
out.write(value);
return;
} else {
out.write((value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
protected void writeStream(InputStream in) throws IOException {
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
}
}

View File

@@ -0,0 +1,39 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
public class ConfigurationMessage {
private final Optional<Boolean> readReceipts;
private final Optional<Boolean> unidentifiedDeliveryIndicators;
private final Optional<Boolean> typingIndicators;
private final Optional<Boolean> linkPreviews;
public ConfigurationMessage(Optional<Boolean> readReceipts,
Optional<Boolean> unidentifiedDeliveryIndicators,
Optional<Boolean> typingIndicators,
Optional<Boolean> linkPreviews)
{
this.readReceipts = readReceipts;
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
this.typingIndicators = typingIndicators;
this.linkPreviews = linkPreviews;
}
public Optional<Boolean> getReadReceipts() {
return readReceipts;
}
public Optional<Boolean> getUnidentifiedDeliveryIndicators() {
return unidentifiedDeliveryIndicators;
}
public Optional<Boolean> getTypingIndicators() {
return typingIndicators;
}
public Optional<Boolean> getLinkPreviews() {
return linkPreviews;
}
}

View File

@@ -0,0 +1,23 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
public class ContactsMessage {
private final SignalServiceAttachment contacts;
private final boolean complete;
public ContactsMessage(SignalServiceAttachment contacts, boolean complete) {
this.contacts = contacts;
this.complete = complete;
}
public SignalServiceAttachment getContactsStream() {
return contacts;
}
public boolean isComplete() {
return complete;
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class DeviceContact {
private final SignalServiceAddress address;
private final Optional<String> name;
private final Optional<SignalServiceAttachmentStream> avatar;
private final Optional<String> color;
private final Optional<VerifiedMessage> verified;
private final Optional<byte[]> profileKey;
private final boolean blocked;
private final Optional<Integer> expirationTimer;
public DeviceContact(SignalServiceAddress address, Optional<String> name,
Optional<SignalServiceAttachmentStream> avatar,
Optional<String> color,
Optional<VerifiedMessage> verified,
Optional<byte[]> profileKey,
boolean blocked,
Optional<Integer> expirationTimer)
{
this.address = address;
this.name = name;
this.avatar = avatar;
this.color = color;
this.verified = verified;
this.profileKey = profileKey;
this.blocked = blocked;
this.expirationTimer = expirationTimer;
}
public Optional<SignalServiceAttachmentStream> getAvatar() {
return avatar;
}
public Optional<String> getName() {
return name;
}
public SignalServiceAddress getAddress() {
return address;
}
public Optional<String> getColor() {
return color;
}
public Optional<VerifiedMessage> getVerified() {
return verified;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public boolean isBlocked() {
return blocked;
}
public Optional<Integer> getExpirationTimer() {
return expirationTimer;
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2014-2018 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.io.InputStream;
public class DeviceContactsInputStream extends ChunkedInputStream {
private static final String TAG = DeviceContactsInputStream.class.getSimpleName();
public DeviceContactsInputStream(InputStream in) {
super(in);
}
public DeviceContact read() throws IOException {
long detailsLength = readRawVarint32();
byte[] detailsSerialized = new byte[(int)detailsLength];
Util.readFully(in, detailsSerialized);
SignalServiceProtos.ContactDetails details = SignalServiceProtos.ContactDetails.parseFrom(detailsSerialized);
if (!SignalServiceAddress.isValidAddress(details.getUuid(), details.getNumber())) {
throw new IOException("Missing contact address!");
}
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(details.getUuid()), details.getNumber());
Optional<String> name = Optional.fromNullable(details.getName());
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
Optional<VerifiedMessage> verified = Optional.absent();
Optional<byte[]> profileKey = Optional.absent();
boolean blocked = false;
Optional<Integer> expireTimer = Optional.absent();
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
InputStream avatarStream = new LimitedInputStream(in, avatarLength);
String avatarContentType = details.getAvatar().getContentType();
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
}
if (details.hasVerified()) {
try {
if (!SignalServiceAddress.isValidAddress(details.getVerified().getDestinationUuid(), details.getVerified().getDestinationE164())) {
throw new InvalidMessageException("Missing Verified address!");
}
IdentityKey identityKey = new IdentityKey(details.getVerified().getIdentityKey().toByteArray(), 0);
SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(details.getVerified().getDestinationUuid()),
details.getVerified().getDestinationE164());
VerifiedMessage.VerifiedState state;
switch (details.getVerified().getState()) {
case VERIFIED: state = VerifiedMessage.VerifiedState.VERIFIED; break;
case UNVERIFIED:state = VerifiedMessage.VerifiedState.UNVERIFIED; break;
case DEFAULT: state = VerifiedMessage.VerifiedState.DEFAULT; break;
default: throw new InvalidMessageException("Unknown state: " + details.getVerified().getState());
}
verified = Optional.of(new VerifiedMessage(destination, identityKey, state, System.currentTimeMillis()));
} catch (InvalidKeyException | InvalidMessageException e) {
Log.w(TAG, e);
verified = Optional.absent();
}
}
if (details.hasProfileKey()) {
profileKey = Optional.fromNullable(details.getProfileKey().toByteArray());
}
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
expireTimer = Optional.of(details.getExpireTimer());
}
blocked = details.getBlocked();
return new DeviceContact(address, name, avatar, color, verified, profileKey, blocked, expireTimer);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2014-2018 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import com.google.protobuf.ByteString;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException;
import java.io.OutputStream;
public class DeviceContactsOutputStream extends ChunkedOutputStream {
public DeviceContactsOutputStream(OutputStream out) {
super(out);
}
public void write(DeviceContact contact) throws IOException {
writeContactDetails(contact);
writeAvatarImage(contact);
}
public void close() throws IOException {
out.close();
}
private void writeAvatarImage(DeviceContact contact) throws IOException {
if (contact.getAvatar().isPresent()) {
writeStream(contact.getAvatar().get().getInputStream());
}
}
private void writeContactDetails(DeviceContact contact) throws IOException {
SignalServiceProtos.ContactDetails.Builder contactDetails = SignalServiceProtos.ContactDetails.newBuilder();
if (contact.getAddress().getUuid().isPresent()) {
contactDetails.setUuid(contact.getAddress().getUuid().get().toString());
}
if (contact.getAddress().getNumber().isPresent()) {
contactDetails.setNumber(contact.getAddress().getNumber().get());
}
if (contact.getName().isPresent()) {
contactDetails.setName(contact.getName().get());
}
if (contact.getAvatar().isPresent()) {
SignalServiceProtos.ContactDetails.Avatar.Builder avatarBuilder = SignalServiceProtos.ContactDetails.Avatar.newBuilder();
avatarBuilder.setContentType(contact.getAvatar().get().getContentType());
avatarBuilder.setLength((int)contact.getAvatar().get().getLength());
contactDetails.setAvatar(avatarBuilder);
}
if (contact.getColor().isPresent()) {
contactDetails.setColor(contact.getColor().get());
}
if (contact.getVerified().isPresent()) {
SignalServiceProtos.Verified.State state;
switch (contact.getVerified().get().getVerified()) {
case VERIFIED: state = SignalServiceProtos.Verified.State.VERIFIED; break;
case UNVERIFIED: state = SignalServiceProtos.Verified.State.UNVERIFIED; break;
default: state = SignalServiceProtos.Verified.State.DEFAULT; break;
}
SignalServiceProtos.Verified.Builder verifiedBuilder = SignalServiceProtos.Verified.newBuilder()
.setIdentityKey(ByteString.copyFrom(contact.getVerified().get().getIdentityKey().serialize()))
.setState(state);
if (contact.getVerified().get().getDestination().getUuid().isPresent()) {
verifiedBuilder.setDestinationUuid(contact.getVerified().get().getDestination().getUuid().get().toString());
}
if (contact.getVerified().get().getDestination().getNumber().isPresent()) {
verifiedBuilder.setDestinationE164(contact.getVerified().get().getDestination().getNumber().get());
}
contactDetails.setVerified(verifiedBuilder.build());
}
if (contact.getProfileKey().isPresent()) {
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get()));
}
if (contact.getExpirationTimer().isPresent()) {
contactDetails.setExpireTimer(contact.getExpirationTimer().get());
}
contactDetails.setBlocked(contact.isBlocked());
byte[] serializedContactDetails = contactDetails.build().toByteArray();
writeVarint32(serializedContactDetails.length);
out.write(serializedContactDetails);
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class DeviceGroup {
private final byte[] id;
private final Optional<String> name;
private final List<SignalServiceAddress> members;
private final Optional<SignalServiceAttachmentStream> avatar;
private final boolean active;
private final Optional<Integer> expirationTimer;
private final Optional<String> color;
private final boolean blocked;
public DeviceGroup(byte[] id, Optional<String> name, List<SignalServiceAddress> members,
Optional<SignalServiceAttachmentStream> avatar,
boolean active, Optional<Integer> expirationTimer,
Optional<String> color, boolean blocked)
{
this.id = id;
this.name = name;
this.members = members;
this.avatar = avatar;
this.active = active;
this.expirationTimer = expirationTimer;
this.color = color;
this.blocked = blocked;
}
public Optional<SignalServiceAttachmentStream> getAvatar() {
return avatar;
}
public Optional<String> getName() {
return name;
}
public byte[] getId() {
return id;
}
public List<SignalServiceAddress> getMembers() {
return members;
}
public boolean isActive() {
return active;
}
public Optional<Integer> getExpirationTimer() {
return expirationTimer;
}
public Optional<String> getColor() {
return color;
}
public boolean isBlocked() {
return blocked;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2014-2018 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupDetails;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class DeviceGroupsInputStream extends ChunkedInputStream{
public DeviceGroupsInputStream(InputStream in) {
super(in);
}
public DeviceGroup read() throws IOException {
long detailsLength = readRawVarint32();
byte[] detailsSerialized = new byte[(int)detailsLength];
Util.readFully(in, detailsSerialized);
GroupDetails details = GroupDetails.parseFrom(detailsSerialized);
if (!details.hasId()) {
throw new IOException("ID missing on group record!");
}
byte[] id = details.getId().toByteArray();
Optional<String> name = Optional.fromNullable(details.getName());
List<GroupDetails.Member> members = details.getMembersList();
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
boolean active = details.getActive();
Optional<Integer> expirationTimer = Optional.absent();
Optional<String> color = Optional.fromNullable(details.getColor());
boolean blocked = details.getBlocked();
if (details.hasAvatar()) {
long avatarLength = details.getAvatar().getLength();
InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength);
String avatarContentType = details.getAvatar().getContentType();
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
}
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
expirationTimer = Optional.of(details.getExpireTimer());
}
List<SignalServiceAddress> addressMembers = new ArrayList<>(members.size());
for (GroupDetails.Member member : members) {
if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) {
addressMembers.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164()));
} else {
throw new IOException("Missing group member address!");
}
}
return new DeviceGroup(id, name, addressMembers, avatar, active, expirationTimer, color, blocked);
}
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import com.google.protobuf.ByteString;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupDetails;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class DeviceGroupsOutputStream extends ChunkedOutputStream {
public DeviceGroupsOutputStream(OutputStream out) {
super(out);
}
public void write(DeviceGroup group) throws IOException {
writeGroupDetails(group);
writeAvatarImage(group);
}
public void close() throws IOException {
out.close();
}
private void writeAvatarImage(DeviceGroup contact) throws IOException {
if (contact.getAvatar().isPresent()) {
writeStream(contact.getAvatar().get().getInputStream());
}
}
private void writeGroupDetails(DeviceGroup group) throws IOException {
GroupDetails.Builder groupDetails = GroupDetails.newBuilder();
groupDetails.setId(ByteString.copyFrom(group.getId()));
if (group.getName().isPresent()) {
groupDetails.setName(group.getName().get());
}
if (group.getAvatar().isPresent()) {
GroupDetails.Avatar.Builder avatarBuilder = GroupDetails.Avatar.newBuilder();
avatarBuilder.setContentType(group.getAvatar().get().getContentType());
avatarBuilder.setLength((int)group.getAvatar().get().getLength());
groupDetails.setAvatar(avatarBuilder);
}
if (group.getExpirationTimer().isPresent()) {
groupDetails.setExpireTimer(group.getExpirationTimer().get());
}
if (group.getColor().isPresent()) {
groupDetails.setColor(group.getColor().get());
}
List<GroupDetails.Member> members = new ArrayList<>(group.getMembers().size());
List<String> membersE164 = new ArrayList<>(group.getMembers().size());
for (SignalServiceAddress address : group.getMembers()) {
GroupDetails.Member.Builder builder = GroupDetails.Member.newBuilder();
if (address.getUuid().isPresent()) {
builder.setUuid(address.getUuid().get().toString());
}
if (address.getNumber().isPresent()) {
builder.setE164(address.getNumber().get());
membersE164.add(address.getNumber().get());
}
members.add(builder.build());
}
groupDetails.addAllMembers(members);
groupDetails.addAllMembersE164(membersE164);
groupDetails.setActive(group.isActive());
groupDetails.setBlocked(group.isBlocked());
byte[] serializedContactDetails = groupDetails.build().toByteArray();
writeVarint32(serializedContactDetails.length);
out.write(serializedContactDetails);
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeviceInfo {
@JsonProperty
private long id;
@JsonProperty
private String name;
@JsonProperty
private long created;
@JsonProperty
private long lastSeen;
public DeviceInfo() {}
public long getId() {
return id;
}
public String getName() {
return name;
}
public long getCreated() {
return created;
}
public long getLastSeen() {
return lastSeen;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
public class KeysMessage {
private final Optional<byte[]> storageService;
public KeysMessage(Optional<byte[]> storageService) {
this.storageService = storageService;
}
public Optional<byte[]> getStorageService() {
return storageService;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class ReadMessage {
private final SignalServiceAddress sender;
private final long timestamp;
public ReadMessage(SignalServiceAddress sender, long timestamp) {
this.sender = sender;
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
public SignalServiceAddress getSender() {
return sender;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.Request;
public class RequestMessage {
private final Request request;
public RequestMessage(Request request) {
this.request = request;
}
public boolean isContactsRequest() {
return request.getType() == Request.Type.CONTACTS;
}
public boolean isGroupsRequest() {
return request.getType() == Request.Type.GROUPS;
}
public boolean isBlockedListRequest() {
return request.getType() == Request.Type.BLOCKED;
}
public boolean isConfigurationRequest() {
return request.getType() == Request.Type.CONFIGURATION;
}
public boolean isKeysRequest() {
return request.getType() == Request.Type.KEYS;
}
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class SentTranscriptMessage {
private final Optional<SignalServiceAddress> destination;
private final long timestamp;
private final long expirationStartTimestamp;
private final SignalServiceDataMessage message;
private final Map<String, Boolean> unidentifiedStatusByUuid;
private final Map<String, Boolean> unidentifiedStatusByE164;
private final Set<SignalServiceAddress> recipients;
private final boolean isRecipientUpdate;
public SentTranscriptMessage(Optional<SignalServiceAddress> destination, long timestamp, SignalServiceDataMessage message,
long expirationStartTimestamp, Map<SignalServiceAddress, Boolean> unidentifiedStatus,
boolean isRecipientUpdate)
{
this.destination = destination;
this.timestamp = timestamp;
this.message = message;
this.expirationStartTimestamp = expirationStartTimestamp;
this.unidentifiedStatusByUuid = new HashMap<>();
this.unidentifiedStatusByE164 = new HashMap<>();
this.recipients = unidentifiedStatus.keySet();
this.isRecipientUpdate = isRecipientUpdate;
for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) {
if (entry.getKey().getUuid().isPresent()) {
unidentifiedStatusByUuid.put(entry.getKey().getUuid().get().toString(), entry.getValue());
}
if (entry.getKey().getNumber().isPresent()) {
unidentifiedStatusByE164.put(entry.getKey().getNumber().get(), entry.getValue());
}
}
}
public Optional<SignalServiceAddress> getDestination() {
return destination;
}
public long getTimestamp() {
return timestamp;
}
public long getExpirationStartTimestamp() {
return expirationStartTimestamp;
}
public SignalServiceDataMessage getMessage() {
return message;
}
public boolean isUnidentified(UUID uuid) {
return isUnidentified(uuid.toString());
}
public boolean isUnidentified(String destination) {
if (unidentifiedStatusByUuid.containsKey(destination)) {
return unidentifiedStatusByUuid.get(destination);
} else if (unidentifiedStatusByE164.containsKey(destination)) {
return unidentifiedStatusByE164.get(destination);
} else {
return false;
}
}
public Set<SignalServiceAddress> getRecipients() {
return recipients;
}
public boolean isRecipientUpdate() {
return isRecipientUpdate;
}
}

View File

@@ -0,0 +1,322 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import java.util.LinkedList;
import java.util.List;
public class SignalServiceSyncMessage {
private final Optional<SentTranscriptMessage> sent;
private final Optional<ContactsMessage> contacts;
private final Optional<SignalServiceAttachment> groups;
private final Optional<BlockedListMessage> blockedList;
private final Optional<RequestMessage> request;
private final Optional<List<ReadMessage>> reads;
private final Optional<ViewOnceOpenMessage> viewOnceOpen;
private final Optional<VerifiedMessage> verified;
private final Optional<ConfigurationMessage> configuration;
private final Optional<List<StickerPackOperationMessage>> stickerPackOperations;
private final Optional<FetchType> fetchType;
private final Optional<KeysMessage> keys;
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<ContactsMessage> contacts,
Optional<SignalServiceAttachment> groups,
Optional<BlockedListMessage> blockedList,
Optional<RequestMessage> request,
Optional<List<ReadMessage>> reads,
Optional<ViewOnceOpenMessage> viewOnceOpen,
Optional<VerifiedMessage> verified,
Optional<ConfigurationMessage> configuration,
Optional<List<StickerPackOperationMessage>> stickerPackOperations,
Optional<FetchType> fetchType,
Optional<KeysMessage> keys)
{
this.sent = sent;
this.contacts = contacts;
this.groups = groups;
this.blockedList = blockedList;
this.request = request;
this.reads = reads;
this.viewOnceOpen = viewOnceOpen;
this.verified = verified;
this.configuration = configuration;
this.stickerPackOperations = stickerPackOperations;
this.fetchType = fetchType;
this.keys = keys;
}
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
return new SignalServiceSyncMessage(Optional.of(sent),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forContacts(ContactsMessage contacts) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.of(contacts),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forGroups(SignalServiceAttachment groups) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.of(groups),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forRequest(RequestMessage request) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.of(request),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forRead(List<ReadMessage> reads) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.of(reads),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.of(timerRead),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forRead(ReadMessage read) {
List<ReadMessage> reads = new LinkedList<>();
reads.add(read);
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.of(reads),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forVerified(VerifiedMessage verifiedMessage) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.of(verifiedMessage),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forBlocked(BlockedListMessage blocked) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.of(blocked),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forConfiguration(ConfigurationMessage configuration) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.of(configuration),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forStickerPackOperations(List<StickerPackOperationMessage> stickerPackOperations) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.of(stickerPackOperations),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forFetchLatest(FetchType fetchType) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.of(fetchType),
Optional.<KeysMessage>absent());
}
public static SignalServiceSyncMessage forKeys(KeysMessage keys) {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.of(keys));
}
public static SignalServiceSyncMessage empty() {
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
Optional.<ContactsMessage>absent(),
Optional.<SignalServiceAttachment>absent(),
Optional.<BlockedListMessage>absent(),
Optional.<RequestMessage>absent(),
Optional.<List<ReadMessage>>absent(),
Optional.<ViewOnceOpenMessage>absent(),
Optional.<VerifiedMessage>absent(),
Optional.<ConfigurationMessage>absent(),
Optional.<List<StickerPackOperationMessage>>absent(),
Optional.<FetchType>absent(),
Optional.<KeysMessage>absent());
}
public Optional<SentTranscriptMessage> getSent() {
return sent;
}
public Optional<SignalServiceAttachment> getGroups() {
return groups;
}
public Optional<ContactsMessage> getContacts() {
return contacts;
}
public Optional<RequestMessage> getRequest() {
return request;
}
public Optional<List<ReadMessage>> getRead() {
return reads;
}
public Optional<ViewOnceOpenMessage> getViewOnceOpen() {
return viewOnceOpen;
}
public Optional<BlockedListMessage> getBlockedList() {
return blockedList;
}
public Optional<VerifiedMessage> getVerified() {
return verified;
}
public Optional<ConfigurationMessage> getConfiguration() {
return configuration;
}
public Optional<List<StickerPackOperationMessage>> getStickerPackOperations() {
return stickerPackOperations;
}
public Optional<FetchType> getFetchType() {
return fetchType;
}
public Optional<KeysMessage> getKeys() {
return keys;
}
public enum FetchType {
LOCAL_PROFILE,
STORAGE_MANIFEST
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
public class StickerPackOperationMessage {
private final Optional<byte[]> packId;
private final Optional<byte[]> packKey;
private final Optional<Type> type;
public StickerPackOperationMessage(byte[] packId, byte[] packKey, Type type) {
this.packId = Optional.fromNullable(packId);
this.packKey = Optional.fromNullable(packKey);
this.type = Optional.fromNullable(type);
}
public Optional<byte[]> getPackId() {
return packId;
}
public Optional<byte[]> getPackKey() {
return packKey;
}
public Optional<Type> getType() {
return type;
}
public enum Type {
INSTALL, REMOVE
}
}

View File

@@ -0,0 +1,40 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class VerifiedMessage {
public enum VerifiedState {
DEFAULT, VERIFIED, UNVERIFIED
}
private final SignalServiceAddress destination;
private final IdentityKey identityKey;
private final VerifiedState verified;
private final long timestamp;
public VerifiedMessage(SignalServiceAddress destination, IdentityKey identityKey, VerifiedState verified, long timestamp) {
this.destination = destination;
this.identityKey = identityKey;
this.verified = verified;
this.timestamp = timestamp;
}
public SignalServiceAddress getDestination() {
return destination;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public VerifiedState getVerified() {
return verified;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@@ -0,0 +1,23 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class ViewOnceOpenMessage {
private final SignalServiceAddress sender;
private final long timestamp;
public ViewOnceOpenMessage(SignalServiceAddress sender, long timestamp) {
this.sender = sender;
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
public SignalServiceAddress getSender() {
return sender;
}
}

View File

@@ -0,0 +1,513 @@
package org.whispersystems.signalservice.api.messages.shared;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import java.util.LinkedList;
import java.util.List;
public class SharedContact {
private final Name name;
private final Optional<Avatar> avatar;
private final Optional<List<Phone>> phone;
private final Optional<List<Email>> email;
private final Optional<List<PostalAddress>> address;
private final Optional<String> organization;
public SharedContact(Name name,
Optional<Avatar> avatar,
Optional<List<Phone>> phone,
Optional<List<Email>> email,
Optional<List<PostalAddress>> address,
Optional<String> organization)
{
this.name = name;
this.avatar = avatar;
this.phone = phone;
this.email = email;
this.address = address;
this.organization = organization;
}
public static Builder newBuilder() {
return new Builder();
}
public Name getName() {
return name;
}
public Optional<Avatar> getAvatar() {
return avatar;
}
public Optional<List<Phone>> getPhone() {
return phone;
}
public Optional<List<Email>> getEmail() {
return email;
}
public Optional<List<PostalAddress>> getAddress() {
return address;
}
public Optional<String> getOrganization() {
return organization;
}
public static class Avatar {
private final SignalServiceAttachment attachment;
private final boolean isProfile;
public Avatar(SignalServiceAttachment attachment, boolean isProfile) {
this.attachment = attachment;
this.isProfile = isProfile;
}
public SignalServiceAttachment getAttachment() {
return attachment;
}
public boolean isProfile() {
return isProfile;
}
public static Builder newBuilder() {
return new Builder();
}
public static class Builder {
private SignalServiceAttachment attachment;
private boolean isProfile;
public Builder withAttachment(SignalServiceAttachment attachment) {
this.attachment = attachment;
return this;
}
public Builder withProfileFlag(boolean isProfile) {
this.isProfile = isProfile;
return this;
}
public Avatar build() {
return new Avatar(attachment, isProfile);
}
}
}
public static class Name {
private final Optional<String> display;
private final Optional<String> given;
private final Optional<String> family;
private final Optional<String> prefix;
private final Optional<String> suffix;
private final Optional<String> middle;
public Name(Optional<String> display, Optional<String> given, Optional<String> family, Optional<String> prefix, Optional<String> suffix, Optional<String> middle) {
this.display = display;
this.given = given;
this.family = family;
this.prefix = prefix;
this.suffix = suffix;
this.middle = middle;
}
public static Builder newBuilder() {
return new Builder();
}
public Optional<String> getDisplay() {
return display;
}
public Optional<String> getGiven() {
return given;
}
public Optional<String> getFamily() {
return family;
}
public Optional<String> getPrefix() {
return prefix;
}
public Optional<String> getSuffix() {
return suffix;
}
public Optional<String> getMiddle() {
return middle;
}
public static class Builder {
private String display;
private String given;
private String family;
private String prefix;
private String suffix;
private String middle;
public Builder setDisplay(String display) {
this.display = display;
return this;
}
public Builder setGiven(String given) {
this.given = given;
return this;
}
public Builder setFamily(String family) {
this.family = family;
return this;
}
public Builder setPrefix(String prefix) {
this.prefix = prefix;
return this;
}
public Builder setSuffix(String suffix) {
this.suffix = suffix;
return this;
}
public Builder setMiddle(String middle) {
this.middle = middle;
return this;
}
public Name build() {
return new Name(Optional.fromNullable(display),
Optional.fromNullable(given),
Optional.fromNullable(family),
Optional.fromNullable(prefix),
Optional.fromNullable(suffix),
Optional.fromNullable(middle));
}
}
}
public static class Phone {
public enum Type {
HOME, WORK, MOBILE, CUSTOM
}
private final String value;
private final Type type;
private final Optional<String> label;
public Phone(String value, Type type, Optional<String> label) {
this.value = value;
this.type = type;
this.label = label;
}
public static Builder newBuilder() {
return new Builder();
}
public String getValue() {
return value;
}
public Type getType() {
return type;
}
public Optional<String> getLabel() {
return label;
}
public static class Builder {
private String value;
private Type type;
private String label;
public Builder setValue(String value) {
this.value = value;
return this;
}
public Builder setType(Type type) {
this.type = type;
return this;
}
public Builder setLabel(String label) {
this.label = label;
return this;
}
public Phone build() {
return new Phone(value, type, Optional.fromNullable(label));
}
}
}
public static class Email {
public enum Type {
HOME, WORK, MOBILE, CUSTOM
}
private final String value;
private final Type type;
private final Optional<String> label;
public Email(String value, Type type, Optional<String> label) {
this.value = value;
this.type = type;
this.label = label;
}
public static Builder newBuilder() {
return new Builder();
}
public String getValue() {
return value;
}
public Type getType() {
return type;
}
public Optional<String> getLabel() {
return label;
}
public static class Builder {
private String value;
private Type type;
private String label;
public Builder setValue(String value) {
this.value = value;
return this;
}
public Builder setType(Type type) {
this.type = type;
return this;
}
public Builder setLabel(String label) {
this.label = label;
return this;
}
public Email build() {
return new Email(value, type, Optional.fromNullable(label));
}
}
}
public static class PostalAddress {
public enum Type {
HOME, WORK, CUSTOM
}
private final Type type;
private final Optional<String> label;
private final Optional<String> street;
private final Optional<String> pobox;
private final Optional<String> neighborhood;
private final Optional<String> city;
private final Optional<String> region;
private final Optional<String> postcode;
private final Optional<String> country;
public PostalAddress(Type type, Optional<String> label, Optional<String> street,
Optional<String> pobox, Optional<String> neighborhood,
Optional<String> city, Optional<String> region,
Optional<String> postcode, Optional<String> country)
{
this.type = type;
this.label = label;
this.street = street;
this.pobox = pobox;
this.neighborhood = neighborhood;
this.city = city;
this.region = region;
this.postcode = postcode;
this.country = country;
}
public static Builder newBuilder() {
return new Builder();
}
public Type getType() {
return type;
}
public Optional<String> getLabel() {
return label;
}
public Optional<String> getStreet() {
return street;
}
public Optional<String> getPobox() {
return pobox;
}
public Optional<String> getNeighborhood() {
return neighborhood;
}
public Optional<String> getCity() {
return city;
}
public Optional<String> getRegion() {
return region;
}
public Optional<String> getPostcode() {
return postcode;
}
public Optional<String> getCountry() {
return country;
}
public static class Builder {
private Type type;
private String label;
private String street;
private String pobox;
private String neighborhood;
private String city;
private String region;
private String postcode;
private String country;
public Builder setType(Type type) {
this.type = type;
return this;
}
public Builder setLabel(String label) {
this.label = label;
return this;
}
public Builder setStreet(String street) {
this.street = street;
return this;
}
public Builder setPobox(String pobox) {
this.pobox = pobox;
return this;
}
public Builder setNeighborhood(String neighborhood) {
this.neighborhood = neighborhood;
return this;
}
public Builder setCity(String city) {
this.city = city;
return this;
}
public Builder setRegion(String region) {
this.region = region;
return this;
}
public Builder setPostcode(String postcode) {
this.postcode = postcode;
return this;
}
public Builder setCountry(String country) {
this.country = country;
return this;
}
public PostalAddress build() {
return new PostalAddress(type, Optional.fromNullable(label), Optional.fromNullable(street),
Optional.fromNullable(pobox), Optional.fromNullable(neighborhood),
Optional.fromNullable(city), Optional.fromNullable(region),
Optional.fromNullable(postcode), Optional.fromNullable(country));
}
}
}
public static class Builder {
private Name name;
private Avatar avatar;
private String organization;
private List<Phone> phone = new LinkedList<>();
private List<Email> email = new LinkedList<>();
private List<PostalAddress> address = new LinkedList<>();
public Builder setName(Name name) {
this.name = name;
return this;
}
public Builder withOrganization(String organization) {
this.organization = organization;
return this;
}
public Builder setAvatar(Avatar avatar) {
this.avatar = avatar;
return this;
}
public Builder withPhone(Phone phone) {
this.phone.add(phone);
return this;
}
public Builder withPhones(List<Phone> phones) {
this.phone.addAll(phones);
return this;
}
public Builder withEmail(Email email) {
this.email.add(email);
return this;
}
public Builder withEmails(List<Email> emails) {
this.email.addAll(emails);
return this;
}
public Builder withAddress(PostalAddress address) {
this.address.add(address);
return this;
}
public Builder withAddresses(List<PostalAddress> addresses) {
this.address.addAll(addresses);
return this;
}
public SharedContact build() {
return new SharedContact(name, Optional.fromNullable(avatar),
phone.isEmpty() ? Optional.<List<Phone>>absent() : Optional.of(phone),
email.isEmpty() ? Optional.<List<Email>>absent() : Optional.of(email),
address.isEmpty() ? Optional.<List<PostalAddress>>absent() : Optional.of(address),
Optional.fromNullable(organization));
}
}
}

View File

@@ -0,0 +1,84 @@
package org.whispersystems.signalservice.api.profiles;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.util.UUID;
public class SignalServiceProfile {
@JsonProperty
private String identityKey;
@JsonProperty
private String name;
@JsonProperty
private String avatar;
@JsonProperty
private String unidentifiedAccess;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private Capabilities capabilities;
@JsonProperty
private String username;
@JsonProperty
@JsonSerialize(using = JsonUtil.UuidSerializer.class)
@JsonDeserialize(using = JsonUtil.UuidDeserializer.class)
private UUID uuid;
public SignalServiceProfile() {}
public String getIdentityKey() {
return identityKey;
}
public String getName() {
return name;
}
public String getAvatar() {
return avatar;
}
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
public Capabilities getCapabilities() {
return capabilities;
}
public String getUsername() {
return username;
}
public UUID getUuid() {
return uuid;
}
public static class Capabilities {
@JsonProperty
private boolean uuid;
public Capabilities() {}
public boolean isUuid() {
return uuid;
}
}
}

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