2 Copyright (C) 2015-2020 AsamK and contributors
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 package org
.asamk
.signal
.manager
;
19 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
21 import org
.asamk
.Signal
;
22 import org
.asamk
.signal
.AttachmentInvalidException
;
23 import org
.asamk
.signal
.GroupNotFoundException
;
24 import org
.asamk
.signal
.NotAGroupMemberException
;
25 import org
.asamk
.signal
.StickerPackInvalidException
;
26 import org
.asamk
.signal
.TrustLevel
;
27 import org
.asamk
.signal
.storage
.SignalAccount
;
28 import org
.asamk
.signal
.storage
.contacts
.ContactInfo
;
29 import org
.asamk
.signal
.storage
.groups
.GroupInfo
;
30 import org
.asamk
.signal
.storage
.groups
.JsonGroupStore
;
31 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
32 import org
.asamk
.signal
.util
.IOUtils
;
33 import org
.asamk
.signal
.util
.Util
;
34 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
35 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
36 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
37 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
41 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
42 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
43 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
44 import org
.signal
.libsignal
.metadata
.SelfSendException
;
45 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
46 import org
.signal
.zkgroup
.InvalidInputException
;
47 import org
.signal
.zkgroup
.VerificationFailedException
;
48 import org
.signal
.zkgroup
.profiles
.ClientZkProfileOperations
;
49 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
50 import org
.whispersystems
.libsignal
.IdentityKey
;
51 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
52 import org
.whispersystems
.libsignal
.InvalidKeyException
;
53 import org
.whispersystems
.libsignal
.InvalidMessageException
;
54 import org
.whispersystems
.libsignal
.InvalidVersionException
;
55 import org
.whispersystems
.libsignal
.ecc
.Curve
;
56 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
57 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
58 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
59 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
60 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
61 import org
.whispersystems
.libsignal
.util
.Medium
;
62 import org
.whispersystems
.libsignal
.util
.Pair
;
63 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
64 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
65 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
66 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
67 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
68 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
69 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
70 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
71 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
72 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
73 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
74 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
75 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
76 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
77 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
78 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
79 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
80 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
81 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
82 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
83 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
84 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
85 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
86 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
87 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
88 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
89 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
90 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
91 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
92 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
93 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
94 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
95 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
96 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
97 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
98 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
99 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
100 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
101 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
102 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.EncapsulatedExceptions
;
103 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
104 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
105 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
106 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
107 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
108 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
109 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
110 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
111 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
112 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
113 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
114 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
115 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
116 import org
.whispersystems
.util
.Base64
;
119 import java
.io
.FileInputStream
;
120 import java
.io
.FileNotFoundException
;
121 import java
.io
.FileOutputStream
;
122 import java
.io
.IOException
;
123 import java
.io
.InputStream
;
124 import java
.io
.OutputStream
;
126 import java
.net
.URISyntaxException
;
127 import java
.net
.URLEncoder
;
128 import java
.nio
.file
.Files
;
129 import java
.nio
.file
.Paths
;
130 import java
.nio
.file
.StandardCopyOption
;
131 import java
.util
.ArrayList
;
132 import java
.util
.Arrays
;
133 import java
.util
.Collection
;
134 import java
.util
.Collections
;
135 import java
.util
.Date
;
136 import java
.util
.HashSet
;
137 import java
.util
.LinkedList
;
138 import java
.util
.List
;
139 import java
.util
.Locale
;
140 import java
.util
.Objects
;
141 import java
.util
.Set
;
142 import java
.util
.UUID
;
143 import java
.util
.concurrent
.TimeUnit
;
144 import java
.util
.concurrent
.TimeoutException
;
145 import java
.util
.stream
.Collectors
;
146 import java
.util
.zip
.ZipEntry
;
147 import java
.util
.zip
.ZipFile
;
149 public class Manager
implements Signal
{
151 private final SleepTimer timer
= new UptimeSleepTimer();
152 private final SignalServiceConfiguration serviceConfiguration
;
153 private final String userAgent
;
155 private final SignalAccount account
;
156 private final PathConfig pathConfig
;
157 private SignalServiceAccountManager accountManager
;
158 private SignalServiceMessagePipe messagePipe
= null;
159 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
161 public Manager(SignalAccount account
, PathConfig pathConfig
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) {
162 this.account
= account
;
163 this.pathConfig
= pathConfig
;
164 this.serviceConfiguration
= serviceConfiguration
;
165 this.userAgent
= userAgent
;
166 this.accountManager
= createSignalServiceAccountManager();
168 this.account
.setResolver(this::resolveSignalServiceAddress
);
171 public String
getUsername() {
172 return account
.getUsername();
175 public SignalServiceAddress
getSelfAddress() {
176 return account
.getSelfAddress();
179 private SignalServiceAccountManager
createSignalServiceAccountManager() {
180 return new SignalServiceAccountManager(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), userAgent
, timer
);
183 private IdentityKeyPair
getIdentityKeyPair() {
184 return account
.getSignalProtocolStore().getIdentityKeyPair();
187 public int getDeviceId() {
188 return account
.getDeviceId();
191 private String
getMessageCachePath() {
192 return pathConfig
.getDataPath() + "/" + account
.getUsername() + ".d/msg-cache";
195 private String
getMessageCachePath(String sender
) {
196 if (sender
== null || sender
.isEmpty()) {
197 return getMessageCachePath();
200 return getMessageCachePath() + "/" + sender
.replace("/", "_");
203 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
204 String cachePath
= getMessageCachePath(sender
);
205 IOUtils
.createPrivateDirectories(cachePath
);
206 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
209 public static Manager
init(String username
, String settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
) throws IOException
{
210 PathConfig pathConfig
= PathConfig
.createDefault(settingsPath
);
212 if (!SignalAccount
.userExists(pathConfig
.getDataPath(), username
)) {
213 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
214 int registrationId
= KeyHelper
.generateRegistrationId(false);
216 ProfileKey profileKey
= KeyUtils
.createProfileKey();
217 SignalAccount account
= SignalAccount
.create(pathConfig
.getDataPath(), username
, identityKey
, registrationId
, profileKey
);
220 return new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
223 SignalAccount account
= SignalAccount
.load(pathConfig
.getDataPath(), username
);
225 Manager m
= new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
227 m
.migrateLegacyConfigs();
228 m
.checkAccountState();
233 private void migrateLegacyConfigs() {
234 // Copy group avatars that were previously stored in the attachments folder
235 // to the new avatar folder
236 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
237 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
238 File avatarFile
= getGroupAvatarFile(g
.groupId
);
239 File attachmentFile
= getAttachmentFile(new SignalServiceAttachmentRemoteId(g
.getAvatarId()));
240 if (!avatarFile
.exists() && attachmentFile
.exists()) {
242 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
243 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
244 } catch (Exception e
) {
249 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
252 if (account
.getProfileKey() == null) {
253 // Old config file, creating new profile key
254 account
.setProfileKey(KeyUtils
.createProfileKey());
259 private void checkAccountState() throws IOException
{
260 if (account
.isRegistered()) {
261 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
265 if (account
.getUuid() == null) {
266 account
.setUuid(accountManager
.getOwnUuid());
272 public boolean isRegistered() {
273 return account
.isRegistered();
276 public void register(boolean voiceVerification
) throws IOException
{
277 account
.setPassword(KeyUtils
.createPassword());
279 // Resetting UUID, because registering doesn't work otherwise
280 account
.setUuid(null);
281 accountManager
= createSignalServiceAccountManager();
283 if (voiceVerification
) {
284 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
286 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
289 account
.setRegistered(false);
293 public void updateAccountAttributes() throws IOException
{
294 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig
.capabilities
);
297 public void setProfileName(String name
) throws IOException
{
298 accountManager
.setProfileName(account
.getProfileKey(), name
);
301 public void setProfileAvatar(File avatar
) throws IOException
{
302 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
303 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
304 streamDetails
.getStream().close();
307 public void removeProfileAvatar() throws IOException
{
308 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
311 public void unregister() throws IOException
{
312 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
313 // If this is the master device, other users can't send messages to this number anymore.
314 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
315 accountManager
.setGcmId(Optional
.absent());
317 account
.setRegistered(false);
321 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
322 List
<DeviceInfo
> devices
= accountManager
.getDevices();
323 account
.setMultiDevice(devices
.size() > 1);
328 public void removeLinkedDevices(int deviceId
) throws IOException
{
329 accountManager
.removeDevice(deviceId
);
330 List
<DeviceInfo
> devices
= accountManager
.getDevices();
331 account
.setMultiDevice(devices
.size() > 1);
335 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
336 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
338 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
341 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
342 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
343 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
345 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
346 account
.setMultiDevice(true);
350 private List
<PreKeyRecord
> generatePreKeys() {
351 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
353 final int offset
= account
.getPreKeyIdOffset();
354 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
355 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
356 ECKeyPair keyPair
= Curve
.generateKeyPair();
357 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
362 account
.addPreKeys(records
);
368 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
370 ECKeyPair keyPair
= Curve
.generateKeyPair();
371 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
372 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
374 account
.addSignedPreKey(record);
378 } catch (InvalidKeyException e
) {
379 throw new AssertionError(e
);
383 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
384 verificationCode
= verificationCode
.replace("-", "");
385 account
.setSignalingKey(KeyUtils
.createSignalingKey());
386 // TODO make unrestricted unidentified access configurable
387 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig
.capabilities
);
389 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
390 // TODO response.isStorageCapable()
391 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
392 account
.setRegistered(true);
393 account
.setUuid(uuid
);
394 account
.setRegistrationLockPin(pin
);
395 account
.getSignalProtocolStore().saveIdentity(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel
.TRUSTED_VERIFIED
);
401 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
402 if (pin
.isPresent()) {
403 account
.setRegistrationLockPin(pin
.get());
404 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
406 account
.setRegistrationLockPin(null);
407 accountManager
.removeRegistrationLockV1();
412 void refreshPreKeys() throws IOException
{
413 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
414 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
415 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
417 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
420 private SignalServiceMessageReceiver
getMessageReceiver() {
421 // TODO implement ZkGroup support
422 final ClientZkProfileOperations clientZkProfileOperations
= null;
423 return new SignalServiceMessageReceiver(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), userAgent
, null, timer
, clientZkProfileOperations
);
426 private SignalServiceMessageSender
getMessageSender() {
427 // TODO implement ZkGroup support
428 final ClientZkProfileOperations clientZkProfileOperations
= null;
429 final boolean attachmentsV3
= false;
430 return new SignalServiceMessageSender(serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
431 account
.getDeviceId(), account
.getSignalProtocolStore(), userAgent
, account
.isMultiDevice(), attachmentsV3
, Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent(), clientZkProfileOperations
);
434 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
435 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
440 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
441 } catch (IOException ignored
) {
445 SignalServiceMessageReceiver receiver
= getMessageReceiver();
447 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
448 } catch (VerificationFailedException e
) {
449 throw new AssertionError(e
);
453 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
454 File file
= getGroupAvatarFile(groupId
);
455 if (!file
.exists()) {
456 return Optional
.absent();
459 return Optional
.of(Utils
.createAttachment(file
));
462 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
463 File file
= getContactAvatarFile(number
);
464 if (!file
.exists()) {
465 return Optional
.absent();
468 return Optional
.of(Utils
.createAttachment(file
));
471 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
472 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
474 throw new GroupNotFoundException(groupId
);
476 if (!g
.isMember(account
.getSelfAddress())) {
477 throw new NotAGroupMemberException(groupId
, g
.name
);
482 public List
<GroupInfo
> getGroups() {
483 return account
.getGroupStore().getGroups();
487 public long sendGroupMessage(String messageText
, List
<String
> attachments
,
489 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
490 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
491 if (attachments
!= null) {
492 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
494 if (groupId
!= null) {
495 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
498 messageBuilder
.asGroupMessage(group
);
501 final GroupInfo g
= getGroupForSending(groupId
);
503 messageBuilder
.withExpiration(g
.messageExpirationTime
);
505 return sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
508 public void sendGroupMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
509 long targetSentTimestamp
, byte[] groupId
)
510 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
511 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
512 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
513 .withReaction(reaction
);
514 if (groupId
!= null) {
515 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
518 messageBuilder
.asGroupMessage(group
);
520 final GroupInfo g
= getGroupForSending(groupId
);
521 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
524 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
525 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
529 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
530 .asGroupMessage(group
);
532 final GroupInfo g
= getGroupForSending(groupId
);
533 g
.removeMember(account
.getSelfAddress());
534 account
.getGroupStore().updateGroup(g
);
536 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
539 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
541 if (groupId
== null) {
543 g
= new GroupInfo(KeyUtils
.createGroupId());
544 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
546 g
= getGroupForSending(groupId
);
553 if (members
!= null) {
554 final Set
<String
> newE164Members
= new HashSet
<>();
555 for (SignalServiceAddress member
: members
) {
556 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
559 newE164Members
.add(member
.getNumber().get());
562 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
563 if (contacts
.size() != newE164Members
.size()) {
564 // Some of the new members are not registered on Signal
565 for (ContactTokenDetails contact
: contacts
) {
566 newE164Members
.remove(contact
.getNumber());
568 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
569 System
.err
.println("Aborting…");
573 g
.addMembers(members
);
576 if (avatarFile
!= null) {
577 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
578 File aFile
= getGroupAvatarFile(g
.groupId
);
579 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
582 account
.getGroupStore().updateGroup(g
);
584 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
586 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
590 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
591 if (groupId
== null) {
594 GroupInfo g
= getGroupForSending(groupId
);
596 if (!g
.isMember(recipient
)) {
600 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
602 // Send group message only to the recipient who requested it
603 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
606 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
607 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
610 .withMembers(new ArrayList
<>(g
.getMembers()));
612 File aFile
= getGroupAvatarFile(g
.groupId
);
613 if (aFile
.exists()) {
615 group
.withAvatar(Utils
.createAttachment(aFile
));
616 } catch (IOException e
) {
617 throw new AttachmentInvalidException(aFile
.toString(), e
);
621 return SignalServiceDataMessage
.newBuilder()
622 .asGroupMessage(group
.build())
623 .withExpiration(g
.messageExpirationTime
);
626 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
627 if (groupId
== null) {
631 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
634 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
635 .asGroupMessage(group
.build());
637 // Send group info request message to the recipient who sent us a message with this groupId
638 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
641 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
642 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
643 Collections
.singletonList(messageId
),
644 System
.currentTimeMillis());
646 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
650 public long sendMessage(String message
, List
<String
> attachments
, String recipient
)
651 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
652 List
<String
> recipients
= new ArrayList
<>(1);
653 recipients
.add(recipient
);
654 return sendMessage(message
, attachments
, recipients
);
658 public long sendMessage(String messageText
, List
<String
> attachments
,
659 List
<String
> recipients
)
660 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
661 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
662 if (attachments
!= null) {
663 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
665 // Upload attachments here, so we only upload once even for multiple recipients
666 SignalServiceMessageSender messageSender
= getMessageSender();
667 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
668 for (SignalServiceAttachment attachment
: attachmentStreams
) {
669 if (attachment
.isStream()) {
670 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
671 } else if (attachment
.isPointer()) {
672 attachmentPointers
.add(attachment
.asPointer());
676 messageBuilder
.withAttachments(attachmentPointers
);
678 return sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
681 public void sendMessageReaction(String emoji
, boolean remove
, String targetAuthor
,
682 long targetSentTimestamp
, List
<String
> recipients
)
683 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
684 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, canonicalizeAndResolveSignalServiceAddress(targetAuthor
), targetSentTimestamp
);
685 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
686 .withReaction(reaction
);
687 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
691 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
692 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
693 .asEndSessionMessage();
695 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
697 sendMessageLegacy(messageBuilder
, signalServiceAddresses
);
698 } catch (Exception e
) {
699 for (SignalServiceAddress address
: signalServiceAddresses
) {
700 handleEndSession(address
);
708 public String
getContactName(String number
) throws InvalidNumberException
{
709 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
710 if (contact
== null) {
718 public void setContactName(String number
, String name
) throws InvalidNumberException
{
719 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
720 ContactInfo contact
= account
.getContactStore().getContact(address
);
721 if (contact
== null) {
722 contact
= new ContactInfo(address
);
723 System
.err
.println("Add contact " + contact
.number
+ " named " + name
);
725 System
.err
.println("Updating contact " + contact
.number
+ " name " + contact
.name
+ " -> " + name
);
728 account
.getContactStore().updateContact(contact
);
733 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
734 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
737 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
738 ContactInfo contact
= account
.getContactStore().getContact(address
);
739 if (contact
== null) {
740 contact
= new ContactInfo(address
);
741 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + address
.getNumber().orNull());
743 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + address
.getNumber().orNull());
745 contact
.blocked
= blocked
;
746 account
.getContactStore().updateContact(contact
);
751 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
752 GroupInfo group
= getGroup(groupId
);
754 throw new GroupNotFoundException(groupId
);
756 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
757 group
.blocked
= blocked
;
758 account
.getGroupStore().updateGroup(group
);
764 public List
<byte[]> getGroupIds() {
765 List
<GroupInfo
> groups
= getGroups();
766 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
767 for (GroupInfo group
: groups
) {
768 ids
.add(group
.groupId
);
774 public String
getGroupName(byte[] groupId
) {
775 GroupInfo group
= getGroup(groupId
);
784 public List
<String
> getGroupMembers(byte[] groupId
) {
785 GroupInfo group
= getGroup(groupId
);
787 return Collections
.emptyList();
789 return new ArrayList
<>(group
.getMembersE164());
794 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
795 if (groupId
.length
== 0) {
798 if (name
.isEmpty()) {
801 if (members
.size() == 0) {
804 if (avatar
.isEmpty()) {
807 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
811 * Change the expiration timer for a contact
813 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) {
814 ContactInfo c
= account
.getContactStore().getContact(address
);
815 c
.messageExpirationTime
= messageExpirationTimer
;
816 account
.getContactStore().updateContact(c
);
820 * Change the expiration timer for a group
822 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
823 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
824 g
.messageExpirationTime
= messageExpirationTimer
;
825 account
.getGroupStore().updateGroup(g
);
829 * Upload the sticker pack from path.
831 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
832 * @return if successful, returns the URL to install the sticker pack in the signal app
834 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
835 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
837 SignalServiceMessageSender messageSender
= getMessageSender();
839 byte[] packKey
= KeyUtils
.createStickerUploadKey();
840 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
843 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
845 } catch (URISyntaxException e
) {
846 throw new AssertionError(e
);
850 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
852 String rootPath
= null;
854 final File file
= new File(path
);
855 if (file
.getName().endsWith(".zip")) {
856 zip
= new ZipFile(file
);
857 } else if (file
.getName().equals("manifest.json")) {
858 rootPath
= file
.getParent();
860 throw new StickerPackInvalidException("Could not find manifest.json");
863 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
865 if (pack
.stickers
== null) {
866 throw new StickerPackInvalidException("Must set a 'stickers' field.");
869 if (pack
.stickers
.isEmpty()) {
870 throw new StickerPackInvalidException("Must include stickers.");
873 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
874 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
875 if (sticker
.file
== null) {
876 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
879 Pair
<InputStream
, Long
> data
;
881 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
882 } catch (IOException ignored
) {
883 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
886 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
887 stickers
.add(stickerInfo
);
890 StickerInfo cover
= null;
891 if (pack
.cover
!= null) {
892 if (pack
.cover
.file
== null) {
893 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
896 Pair
<InputStream
, Long
> data
;
898 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
899 } catch (IOException ignored
) {
900 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
903 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
906 return new SignalServiceStickerManifestUpload(
913 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
914 InputStream inputStream
;
916 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
918 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
920 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
923 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
925 final ZipEntry entry
= zip
.getEntry(subfile
);
926 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
928 final File file
= new File(rootPath
, subfile
);
929 return new Pair
<>(new FileInputStream(file
), file
.length());
933 void requestSyncGroups() throws IOException
{
934 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
935 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
937 sendSyncMessage(message
);
938 } catch (UntrustedIdentityException e
) {
943 void requestSyncContacts() throws IOException
{
944 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
945 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
947 sendSyncMessage(message
);
948 } catch (UntrustedIdentityException e
) {
953 void requestSyncBlocked() throws IOException
{
954 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
955 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
957 sendSyncMessage(message
);
958 } catch (UntrustedIdentityException e
) {
963 void requestSyncConfiguration() throws IOException
{
964 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
965 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
967 sendSyncMessage(message
);
968 } catch (UntrustedIdentityException e
) {
973 private byte[] getSenderCertificate() {
974 // TODO support UUID capable sender certificates
975 // byte[] certificate = accountManager.getSenderCertificate();
978 certificate
= accountManager
.getSenderCertificateLegacy();
979 } catch (IOException e
) {
980 System
.err
.println("Failed to get sender certificate: " + e
);
983 // TODO cache for a day
987 private byte[] getSelfUnidentifiedAccessKey() {
988 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
991 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
992 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
994 return new SignalProfile(
995 encryptedProfile
.getIdentityKey(),
996 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
997 encryptedProfile
.getAvatar(),
998 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
999 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1001 } catch (InvalidCiphertextException e
) {
1006 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) {
1007 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1008 if (contact
== null || contact
.profileKey
== null) {
1011 ProfileKey theirProfileKey
;
1013 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1014 } catch (InvalidInputException
| IOException e
) {
1015 throw new AssertionError(e
);
1017 SignalProfile targetProfile
;
1019 targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1020 } catch (IOException e
) {
1021 System
.err
.println("Failed to get recipient profile: " + e
);
1025 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1029 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1030 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1033 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1036 private Optional
<UnidentifiedAccessPair
> getAccessForSync() {
1037 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1038 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1040 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1041 return Optional
.absent();
1045 return Optional
.of(new UnidentifiedAccessPair(
1046 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1047 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1049 } catch (InvalidCertificateException e
) {
1050 return Optional
.absent();
1054 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) {
1055 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1056 for (SignalServiceAddress recipient
: recipients
) {
1057 result
.add(getAccessFor(recipient
));
1062 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) {
1063 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1064 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1065 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1067 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1068 return Optional
.absent();
1072 return Optional
.of(new UnidentifiedAccessPair(
1073 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1074 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1076 } catch (InvalidCertificateException e
) {
1077 return Optional
.absent();
1081 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(SignalServiceAddress recipient
) {
1082 Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1084 if (unidentifiedAccess
.isPresent()) {
1085 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
1088 return Optional
.absent();
1091 private void sendSyncMessage(SignalServiceSyncMessage message
)
1092 throws IOException
, UntrustedIdentityException
{
1093 SignalServiceMessageSender messageSender
= getMessageSender();
1095 messageSender
.sendMessage(message
, getAccessForSync());
1096 } catch (UntrustedIdentityException e
) {
1097 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1103 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1105 private long sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1106 throws EncapsulatedExceptions
, IOException
{
1107 final long timestamp
= System
.currentTimeMillis();
1108 messageBuilder
.withTimestamp(timestamp
);
1109 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1111 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1112 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1113 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1115 for (SendMessageResult result
: results
) {
1116 if (result
.isUnregisteredFailure()) {
1117 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getLegacyIdentifier(), null));
1118 } else if (result
.isNetworkFailure()) {
1119 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getLegacyIdentifier(), null));
1120 } else if (result
.getIdentityFailure() != null) {
1121 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getLegacyIdentifier(), result
.getIdentityFailure().getIdentityKey()));
1124 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1125 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1130 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1131 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1133 for (String number
: numbers
) {
1134 signalServiceAddresses
.add(canonicalizeAndResolveSignalServiceAddress(number
));
1136 return signalServiceAddresses
;
1139 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1140 throws IOException
{
1141 if (messagePipe
== null) {
1142 messagePipe
= getMessageReceiver().createMessagePipe();
1144 if (unidentifiedMessagePipe
== null) {
1145 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1147 SignalServiceDataMessage message
= null;
1149 SignalServiceMessageSender messageSender
= getMessageSender();
1151 message
= messageBuilder
.build();
1152 if (message
.getGroupContext().isPresent()) {
1154 final boolean isRecipientUpdate
= false;
1155 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1156 for (SendMessageResult r
: result
) {
1157 if (r
.getIdentityFailure() != null) {
1158 account
.getSignalProtocolStore().saveIdentity(r
.getAddress(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1162 } catch (UntrustedIdentityException e
) {
1163 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1164 return Collections
.emptyList();
1166 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1167 SignalServiceAddress recipient
= account
.getSelfAddress();
1168 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1169 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1170 message
.getTimestamp(),
1172 message
.getExpiresInSeconds(),
1173 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1175 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1177 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1179 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1180 } catch (UntrustedIdentityException e
) {
1181 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1182 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1186 // Send to all individually, so sync messages are sent correctly
1187 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1188 for (SignalServiceAddress address
: recipients
) {
1189 ContactInfo contact
= account
.getContactStore().getContact(address
);
1190 if (contact
!= null) {
1191 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1192 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1194 messageBuilder
.withExpiration(0);
1195 messageBuilder
.withProfileKey(null);
1197 message
= messageBuilder
.build();
1199 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1200 results
.add(result
);
1201 } catch (UntrustedIdentityException e
) {
1202 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1203 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1209 if (message
!= null && message
.isEndSession()) {
1210 for (SignalServiceAddress recipient
: recipients
) {
1211 handleEndSession(recipient
);
1218 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1219 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1221 return cipher
.decrypt(envelope
);
1222 } catch (ProtocolUntrustedIdentityException e
) {
1223 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1224 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
.getCause();
1225 account
.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException
.getName()), identityException
.getUntrustedIdentity(), TrustLevel
.UNTRUSTED
);
1226 throw identityException
;
1228 throw new AssertionError(e
);
1232 private void handleEndSession(SignalServiceAddress source
) {
1233 account
.getSignalProtocolStore().deleteAllSessions(source
);
1236 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1237 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1238 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1239 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1240 switch (groupInfo
.getType()) {
1242 if (group
== null) {
1243 group
= new GroupInfo(groupInfo
.getGroupId());
1246 if (groupInfo
.getAvatar().isPresent()) {
1247 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1248 if (avatar
.isPointer()) {
1250 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1251 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1252 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getRemoteId() + "): " + e
.getMessage());
1257 if (groupInfo
.getName().isPresent()) {
1258 group
.name
= groupInfo
.getName().get();
1261 if (groupInfo
.getMembers().isPresent()) {
1262 group
.addMembers(groupInfo
.getMembers().get()
1264 .map(this::resolveSignalServiceAddress
)
1265 .collect(Collectors
.toSet()));
1268 account
.getGroupStore().updateGroup(group
);
1271 if (group
== null) {
1273 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1274 } catch (IOException
| EncapsulatedExceptions e
) {
1275 e
.printStackTrace();
1280 if (group
!= null) {
1281 group
.removeMember(source
);
1282 account
.getGroupStore().updateGroup(group
);
1286 if (group
!= null) {
1288 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1289 } catch (IOException
| EncapsulatedExceptions e
) {
1290 e
.printStackTrace();
1291 } catch (NotAGroupMemberException e
) {
1292 // We have left this group, so don't send a group update message
1298 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1299 if (message
.isEndSession()) {
1300 handleEndSession(conversationPartnerAddress
);
1302 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1303 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1304 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1305 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1306 if (group
== null) {
1307 group
= new GroupInfo(groupInfo
.getGroupId());
1309 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1310 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1311 account
.getGroupStore().updateGroup(group
);
1314 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1315 if (contact
== null) {
1316 contact
= new ContactInfo(conversationPartnerAddress
);
1318 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1319 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1320 account
.getContactStore().updateContact(contact
);
1324 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1325 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1326 if (attachment
.isPointer()) {
1328 retrieveAttachment(attachment
.asPointer());
1329 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1330 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getRemoteId() + "): " + e
.getMessage());
1335 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1336 if (source
.matches(account
.getSelfAddress())) {
1338 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1339 } catch (InvalidInputException ignored
) {
1341 ContactInfo contact
= account
.getContactStore().getContact(source
);
1342 if (contact
!= null) {
1343 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1344 account
.getContactStore().updateContact(contact
);
1347 ContactInfo contact
= account
.getContactStore().getContact(source
);
1348 if (contact
== null) {
1349 contact
= new ContactInfo(source
);
1351 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1352 account
.getContactStore().updateContact(contact
);
1355 if (message
.getPreviews().isPresent()) {
1356 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1357 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1358 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1359 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1361 retrieveAttachment(attachment
);
1362 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1363 System
.err
.println("Failed to retrieve attachment (" + attachment
.getRemoteId() + "): " + e
.getMessage());
1370 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1371 final File cachePath
= new File(getMessageCachePath());
1372 if (!cachePath
.exists()) {
1375 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1376 if (!dir
.isDirectory()) {
1377 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1381 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1382 if (!fileEntry
.isFile()) {
1385 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1387 // Try to delete directory if empty
1392 private void retryFailedReceivedMessage(final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
) {
1393 SignalServiceEnvelope envelope
;
1395 envelope
= Utils
.loadEnvelope(fileEntry
);
1396 if (envelope
== null) {
1399 } catch (IOException e
) {
1400 e
.printStackTrace();
1403 SignalServiceContent content
= null;
1404 if (!envelope
.isReceipt()) {
1406 content
= decryptMessage(envelope
);
1407 } catch (Exception e
) {
1410 handleMessage(envelope
, content
, ignoreAttachments
);
1413 handler
.handleMessage(envelope
, content
, null);
1415 Files
.delete(fileEntry
.toPath());
1416 } catch (IOException e
) {
1417 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1421 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1422 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1423 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1426 if (messagePipe
== null) {
1427 messagePipe
= messageReceiver
.createMessagePipe();
1431 SignalServiceEnvelope envelope
;
1432 SignalServiceContent content
= null;
1433 Exception exception
= null;
1434 final long now
= new Date().getTime();
1436 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1437 // store message on disk, before acknowledging receipt to the server
1439 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1440 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1441 Utils
.storeEnvelope(envelope1
, cacheFile
);
1442 } catch (IOException e
) {
1443 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1446 } catch (TimeoutException e
) {
1447 if (returnOnTimeout
)
1450 } catch (InvalidVersionException e
) {
1451 System
.err
.println("Ignoring error: " + e
.getMessage());
1454 if (!envelope
.isReceipt()) {
1456 content
= decryptMessage(envelope
);
1457 } catch (Exception e
) {
1460 handleMessage(envelope
, content
, ignoreAttachments
);
1463 if (!isMessageBlocked(envelope
, content
)) {
1464 handler
.handleMessage(envelope
, content
, exception
);
1466 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1467 File cacheFile
= null;
1469 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1470 Files
.delete(cacheFile
.toPath());
1471 // Try to delete directory if empty
1472 new File(getMessageCachePath()).delete();
1473 } catch (IOException e
) {
1474 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1479 if (messagePipe
!= null) {
1480 messagePipe
.shutdown();
1486 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1487 SignalServiceAddress source
;
1488 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1489 source
= envelope
.getSourceAddress();
1490 } else if (content
!= null) {
1491 source
= content
.getSender();
1495 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1496 if (sourceContact
!= null && sourceContact
.blocked
) {
1500 if (content
!= null && content
.getDataMessage().isPresent()) {
1501 SignalServiceDataMessage message
= content
.getDataMessage().get();
1502 if (message
.getGroupContext().isPresent() && message
.getGroupContext().get().getGroupV1().isPresent()) {
1503 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1504 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1505 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1513 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1514 if (content
!= null) {
1515 SignalServiceAddress sender
;
1516 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1517 sender
= envelope
.getSourceAddress();
1519 sender
= content
.getSender();
1521 if (content
.getDataMessage().isPresent()) {
1522 SignalServiceDataMessage message
= content
.getDataMessage().get();
1524 if (content
.isNeedsReceipt()) {
1526 sendReceipt(sender
, message
.getTimestamp());
1527 } catch (IOException
| UntrustedIdentityException
| IllegalArgumentException e
) {
1528 e
.printStackTrace();
1532 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1534 if (content
.getSyncMessage().isPresent()) {
1535 account
.setMultiDevice(true);
1536 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1537 if (syncMessage
.getSent().isPresent()) {
1538 SentTranscriptMessage message
= syncMessage
.getSent().get();
1539 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1541 if (syncMessage
.getRequest().isPresent()) {
1542 RequestMessage rm
= syncMessage
.getRequest().get();
1543 if (rm
.isContactsRequest()) {
1546 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1547 e
.printStackTrace();
1550 if (rm
.isGroupsRequest()) {
1553 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1554 e
.printStackTrace();
1557 if (rm
.isBlockedListRequest()) {
1560 } catch (UntrustedIdentityException
| IOException
| IllegalArgumentException e
) {
1561 e
.printStackTrace();
1564 // TODO Handle rm.isConfigurationRequest();
1566 if (syncMessage
.getGroups().isPresent()) {
1567 File tmpFile
= null;
1569 tmpFile
= IOUtils
.createTempFile();
1570 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1571 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1573 while ((g
= s
.read()) != null) {
1574 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1575 if (syncGroup
== null) {
1576 syncGroup
= new GroupInfo(g
.getId());
1578 if (g
.getName().isPresent()) {
1579 syncGroup
.name
= g
.getName().get();
1581 syncGroup
.addMembers(g
.getMembers()
1583 .map(this::resolveSignalServiceAddress
)
1584 .collect(Collectors
.toSet()));
1585 if (!g
.isActive()) {
1586 syncGroup
.removeMember(account
.getSelfAddress());
1588 // Add ourself to the member set as it's marked as active
1589 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1591 syncGroup
.blocked
= g
.isBlocked();
1592 if (g
.getColor().isPresent()) {
1593 syncGroup
.color
= g
.getColor().get();
1596 if (g
.getAvatar().isPresent()) {
1597 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1599 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1600 syncGroup
.archived
= g
.isArchived();
1601 account
.getGroupStore().updateGroup(syncGroup
);
1604 } catch (Exception e
) {
1605 e
.printStackTrace();
1607 if (tmpFile
!= null) {
1609 Files
.delete(tmpFile
.toPath());
1610 } catch (IOException e
) {
1611 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1616 if (syncMessage
.getBlockedList().isPresent()) {
1617 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1618 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1619 setContactBlocked(resolveSignalServiceAddress(address
), true);
1621 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1623 setGroupBlocked(groupId
, true);
1624 } catch (GroupNotFoundException e
) {
1625 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1629 if (syncMessage
.getContacts().isPresent()) {
1630 File tmpFile
= null;
1632 tmpFile
= IOUtils
.createTempFile();
1633 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1634 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1635 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1636 if (contactsMessage
.isComplete()) {
1637 account
.getContactStore().clear();
1640 while ((c
= s
.read()) != null) {
1641 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1642 account
.setProfileKey(c
.getProfileKey().get());
1644 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
1645 ContactInfo contact
= account
.getContactStore().getContact(address
);
1646 if (contact
== null) {
1647 contact
= new ContactInfo(address
);
1649 if (c
.getName().isPresent()) {
1650 contact
.name
= c
.getName().get();
1652 if (c
.getColor().isPresent()) {
1653 contact
.color
= c
.getColor().get();
1655 if (c
.getProfileKey().isPresent()) {
1656 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1658 if (c
.getVerified().isPresent()) {
1659 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1660 account
.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage
.getDestination(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1662 if (c
.getExpirationTimer().isPresent()) {
1663 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1665 contact
.blocked
= c
.isBlocked();
1666 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1667 contact
.archived
= c
.isArchived();
1668 account
.getContactStore().updateContact(contact
);
1670 if (c
.getAvatar().isPresent()) {
1671 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1675 } catch (Exception e
) {
1676 e
.printStackTrace();
1678 if (tmpFile
!= null) {
1680 Files
.delete(tmpFile
.toPath());
1681 } catch (IOException e
) {
1682 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1687 if (syncMessage
.getVerified().isPresent()) {
1688 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1689 account
.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1691 if (syncMessage
.getConfiguration().isPresent()) {
1698 private File
getContactAvatarFile(String number
) {
1699 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
1702 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1703 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1704 if (attachment
.isPointer()) {
1705 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1706 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1708 SignalServiceAttachmentStream stream
= attachment
.asStream();
1709 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1713 private File
getGroupAvatarFile(byte[] groupId
) {
1714 return new File(pathConfig
.getAvatarsPath(), "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1717 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1718 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
1719 if (attachment
.isPointer()) {
1720 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1721 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1723 SignalServiceAttachmentStream stream
= attachment
.asStream();
1724 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1728 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
1729 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
1732 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1733 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
1734 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
1737 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1738 if (storePreview
&& pointer
.getPreview().isPresent()) {
1739 File previewFile
= new File(outputFile
+ ".preview");
1740 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1741 byte[] preview
= pointer
.getPreview().get();
1742 output
.write(preview
, 0, preview
.length
);
1743 } catch (FileNotFoundException e
) {
1744 e
.printStackTrace();
1749 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1751 File tmpFile
= IOUtils
.createTempFile();
1752 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
1753 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1754 byte[] buffer
= new byte[4096];
1757 while ((read
= input
.read(buffer
)) != -1) {
1758 output
.write(buffer
, 0, read
);
1760 } catch (FileNotFoundException e
) {
1761 e
.printStackTrace();
1766 Files
.delete(tmpFile
.toPath());
1767 } catch (IOException e
) {
1768 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1774 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
1775 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1776 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
1780 public boolean isRemote() {
1785 public String
getObjectPath() {
1789 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1790 File groupsFile
= IOUtils
.createTempFile();
1793 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1794 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1795 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1796 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1797 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1798 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1799 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1803 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1804 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1805 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1806 .withStream(groupsFileStream
)
1807 .withContentType("application/octet-stream")
1808 .withLength(groupsFile
.length())
1811 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1816 Files
.delete(groupsFile
.toPath());
1817 } catch (IOException e
) {
1818 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1823 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1824 File contactsFile
= IOUtils
.createTempFile();
1827 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1828 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1829 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1830 VerifiedMessage verifiedMessage
= null;
1831 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore().getIdentity(record.getAddress());
1832 if (currentIdentity
!= null) {
1833 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1836 ProfileKey profileKey
= null;
1838 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1839 } catch (InvalidInputException ignored
) {
1841 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1842 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1843 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1844 Optional
.of(record.messageExpirationTime
),
1845 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1848 if (account
.getProfileKey() != null) {
1849 // Send our own profile key as well
1850 out
.write(new DeviceContact(account
.getSelfAddress(),
1851 Optional
.absent(), Optional
.absent(),
1852 Optional
.absent(), Optional
.absent(),
1853 Optional
.of(account
.getProfileKey()),
1854 false, Optional
.absent(), Optional
.absent(), false));
1858 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1859 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1860 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1861 .withStream(contactsFileStream
)
1862 .withContentType("application/octet-stream")
1863 .withLength(contactsFile
.length())
1866 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1871 Files
.delete(contactsFile
.toPath());
1872 } catch (IOException e
) {
1873 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1878 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1879 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1880 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1881 if (record.blocked
) {
1882 addresses
.add(record.getAddress());
1885 List
<byte[]> groupIds
= new ArrayList
<>();
1886 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1887 if (record.blocked
) {
1888 groupIds
.add(record.groupId
);
1891 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1894 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1895 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1896 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1899 public List
<ContactInfo
> getContacts() {
1900 return account
.getContactStore().getContacts();
1903 public ContactInfo
getContact(String number
) {
1904 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
1907 public GroupInfo
getGroup(byte[] groupId
) {
1908 return account
.getGroupStore().getGroup(groupId
);
1911 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
1912 return account
.getSignalProtocolStore().getIdentities();
1915 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
1916 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
1920 * Trust this the identity with this fingerprint
1922 * @param name username of the identity
1923 * @param fingerprint Fingerprint
1925 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
1926 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1927 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1931 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1932 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1936 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1938 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1939 } catch (IOException
| UntrustedIdentityException e
) {
1940 e
.printStackTrace();
1949 * Trust this the identity with this safety number
1951 * @param name username of the identity
1952 * @param safetyNumber Safety number
1954 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
1955 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
1956 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1960 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1961 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
1965 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1967 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1968 } catch (IOException
| UntrustedIdentityException e
) {
1969 e
.printStackTrace();
1978 * Trust all keys of this identity without verification
1980 * @param name username of the identity
1982 public boolean trustIdentityAllKeys(String name
) {
1983 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
1984 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
1988 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1989 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1990 account
.getSignalProtocolStore().setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1992 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1993 } catch (IOException
| UntrustedIdentityException e
) {
1994 e
.printStackTrace();
2002 public String
computeSafetyNumber(SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
2003 return Utils
.computeSafetyNumber(account
.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress
, theirIdentityKey
);
2006 void saveAccount() {
2010 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2011 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
) ? identifier
: Util
.canonicalizeNumber(identifier
, account
.getUsername());
2012 return resolveSignalServiceAddress(canonicalizedNumber
);
2015 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2016 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2018 return resolveSignalServiceAddress(address
);
2021 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2022 if (address
.matches(account
.getSelfAddress())) {
2023 return account
.getSelfAddress();
2026 return account
.getRecipientStore().resolveServiceAddress(address
);
2029 public interface ReceiveMessageHandler
{
2031 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);