From: AsamK Date: Fri, 31 Dec 2021 15:14:22 +0000 (+0100) Subject: Extract AccountHelper X-Git-Tag: v0.10.1~21 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/ffcda46c31b586c786bae46ddddb405b5e7855cd?ds=inline Extract AccountHelper --- diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index dd5b4156..80bc0de0 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -311,6 +311,10 @@ "allDeclaredMethods":true, "allDeclaredClasses":true} , +{ + "name":"org.asamk.Signal$Error$Failure", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { "name":"org.asamk.Signal$Error$UntrustedIdentity", "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 137e32ad..bc26d87c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -97,6 +97,13 @@ public interface Manager extends Closeable { void checkAccountState() throws IOException; + /** + * This is used for checking a set of phone numbers for registration on Signal + * + * @param numbers The set of phone number in question + * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. + * @throws IOException if it's unable to get the contacts to check if they're registered + */ Map> areUsersRegistered(Set numbers) throws IOException; void updateAccountAttributes(String deviceName) throws IOException; @@ -105,6 +112,13 @@ public interface Manager extends Closeable { void updateConfiguration(Configuration configuration) throws IOException, NotMasterDeviceException; + /** + * @param givenName if null, the previous givenName will be kept + * @param familyName if null, the previous familyName will be kept + * @param about if null, the previous about text will be kept + * @param aboutEmoji if null, the previous about emoji will be kept + * @param avatar if avatar is null the image from the local avatar store is used (if present), + */ void setProfile( String givenName, String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException; @@ -121,7 +135,7 @@ public interface Manager extends Closeable { void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException; - void setRegistrationLockPin(Optional pin) throws IOException; + void setRegistrationLockPin(Optional pin) throws IOException, NotMasterDeviceException; Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException; @@ -191,10 +205,19 @@ public interface Manager extends Closeable { GroupId groupId, boolean blocked ) throws GroupNotFoundException, IOException, NotMasterDeviceException; + /** + * Change the expiration timer for a contact + */ void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer ) throws IOException, UnregisteredRecipientException; + /** + * Upload the sticker pack from path. + * + * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file + * @return if successful, returns the URL to install the sticker pack in the signal app + */ URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException; void requestAllSyncData() throws IOException; @@ -245,18 +268,41 @@ public interface Manager extends Closeable { List getIdentities(RecipientIdentifier.Single recipient); + /** + * Trust this the identity with this fingerprint + * + * @param recipient account of the identity + * @param fingerprint Fingerprint + */ boolean trustIdentityVerified( RecipientIdentifier.Single recipient, byte[] fingerprint ) throws UnregisteredRecipientException; + /** + * Trust this the identity with this safety number + * + * @param recipient account of the identity + * @param safetyNumber Safety number + */ boolean trustIdentityVerifiedSafetyNumber( RecipientIdentifier.Single recipient, String safetyNumber ) throws UnregisteredRecipientException; + /** + * Trust this the identity with this scannable safety number + * + * @param recipient account of the identity + * @param safetyNumber Scannable safety number + */ boolean trustIdentityVerifiedSafetyNumber( RecipientIdentifier.Single recipient, byte[] safetyNumber ) throws UnregisteredRecipientException; + /** + * Trust all keys of this identity without verification + * + * @param recipient account of the identity + */ boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException; void addClosedListener(Runnable listener); diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 1f731ee8..5d091d29 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -52,15 +52,11 @@ import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.push.ACI; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -84,30 +80,26 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.asamk.signal.manager.config.ServiceConfig.capabilities; - public class ManagerImpl implements Manager { private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); - private final SignalDependencies dependencies; - private SignalAccount account; + private final SignalDependencies dependencies; + private final Context context; private final ExecutorService executor = Executors.newCachedThreadPool(); - private final Context context; - private Thread receiveThread; + private boolean isReceivingSynchronous; private final Set weakHandlers = new HashSet<>(); private final Set messageHandlers = new HashSet<>(); private final List closedListeners = new ArrayList<>(); - private boolean isReceivingSynchronous; ManagerImpl( SignalAccount account, @@ -141,13 +133,8 @@ public class ManagerImpl implements Manager { final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath()); this.context = new Context(account, dependencies, avatarStore, attachmentStore, stickerPackStore); - this.context.getReceiveHelper().setAuthenticationFailureListener(() -> { - try { - close(); - } catch (IOException e) { - logger.warn("Failed to close account after authentication failure", e); - } - }); + this.context.getAccountHelper().setUnregisteredListener(this::close); + this.context.getReceiveHelper().setAuthenticationFailureListener(this::close); this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> { synchronized (this) { this.notifyAll(); @@ -162,36 +149,9 @@ public class ManagerImpl implements Manager { @Override public void checkAccountState() throws IOException { - if (account.getLastReceiveTimestamp() == 0) { - logger.info("The Signal protocol expects that incoming messages are regularly received."); - } else { - var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); - long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); - if (days > 7) { - logger.warn( - "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", - days); - } - } - try { - context.getPreKeyHelper().refreshPreKeysIfNecessary(); - if (account.getAci() == null) { - account.setAci(ACI.parseOrNull(dependencies.getAccountManager().getWhoAmI().getAci())); - } - updateAccountAttributes(null); - } catch (AuthorizationFailedException e) { - account.setRegistered(false); - throw e; - } + context.getAccountHelper().checkAccountState(); } - /** - * This is used for checking a set of phone numbers for registration on Signal - * - * @param numbers The set of phone number in question - * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. - * @throws IOException if it's unable to get the contacts to check if they're registered - */ @Override public Map> areUsersRegistered(Set numbers) throws IOException { final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { @@ -222,34 +182,16 @@ public class ManagerImpl implements Manager { @Override public void updateAccountAttributes(String deviceName) throws IOException { - final String encryptedDeviceName; - if (deviceName == null) { - encryptedDeviceName = account.getEncryptedDeviceName(); - } else { - final var privateKey = account.getIdentityKeyPair().getPrivateKey(); - encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); - account.setEncryptedDeviceName(encryptedDeviceName); + if (deviceName != null) { + context.getAccountHelper().setDeviceName(deviceName); } - dependencies.getAccountManager() - .setAccountAttributes(encryptedDeviceName, - null, - account.getLocalRegistrationId(), - true, - null, - account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - capabilities, - account.isDiscoverableByPhoneNumber()); + context.getAccountHelper().updateAccountAttributes(); } @Override public Configuration getConfiguration() { final var configurationStore = account.getConfigurationStore(); - return new Configuration(java.util.Optional.ofNullable(configurationStore.getReadReceipts()), - java.util.Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()), - java.util.Optional.ofNullable(configurationStore.getTypingIndicators()), - java.util.Optional.ofNullable(configurationStore.getLinkPreviews())); + return Configuration.from(configurationStore); } @Override @@ -276,13 +218,6 @@ public class ManagerImpl implements Manager { context.getSyncHelper().sendConfigurationMessage(); } - /** - * @param givenName if null, the previous givenName will be kept - * @param familyName if null, the previous familyName will be kept - * @param about if null, the previous about text will be kept - * @param aboutEmoji if null, the previous about emoji will be kept - * @param avatar if avatar is null the image from the local avatar store is used (if present), - */ @Override public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional avatar @@ -298,28 +233,12 @@ public class ManagerImpl implements Manager { @Override public void unregister() throws IOException { - // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. - // If this is the master device, other users can't send messages to this number anymore. - // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - dependencies.getAccountManager().setGcmId(Optional.absent()); - - account.setRegistered(false); - close(); + context.getAccountHelper().unregister(); } @Override public void deleteAccount() throws IOException { - try { - context.getPinHelper().removeRegistrationLockPin(); - } catch (IOException e) { - logger.warn("Failed to remove registration lock pin"); - } - account.setRegistrationLockPin(null, null); - - dependencies.getAccountManager().deleteAccount(); - - account.setRegistered(false); - close(); + context.getAccountHelper().deleteAccount(); } @Override @@ -353,55 +272,24 @@ public class ManagerImpl implements Manager { @Override public void removeLinkedDevices(long deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); + context.getAccountHelper().removeLinkedDevices(deviceId); } @Override public void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException { - var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); - - addDevice(info.deviceIdentifier(), info.deviceKey()); - } - - private void addDevice( - String deviceIdentifier, ECPublicKey deviceKey - ) throws IOException, InvalidDeviceLinkException { - var identityKeyPair = account.getIdentityKeyPair(); - var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); - - try { - dependencies.getAccountManager() - .addDevice(deviceIdentifier, - deviceKey, - identityKeyPair, - Optional.of(account.getProfileKey().serialize()), - verificationCode); - } catch (InvalidKeyException e) { - throw new InvalidDeviceLinkException("Invalid device link", e); - } - account.setMultiDevice(true); + var deviceLinkInfo = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + context.getAccountHelper().addDevice(deviceLinkInfo); } @Override - public void setRegistrationLockPin(java.util.Optional pin) throws IOException { + public void setRegistrationLockPin(java.util.Optional pin) throws IOException, NotMasterDeviceException { if (!account.isMasterDevice()) { - throw new RuntimeException("Only master device can set a PIN"); + throw new NotMasterDeviceException(); } if (pin.isPresent()) { - final var masterKey = account.getPinMasterKey() != null - ? account.getPinMasterKey() - : KeyUtils.createMasterKey(); - - context.getPinHelper().setRegistrationLockPin(pin.get(), masterKey); - - account.setRegistrationLockPin(pin.get(), masterKey); + context.getAccountHelper().setRegistrationPin(pin.get()); } else { - // Remove KBS Pin - context.getPinHelper().removeRegistrationLockPin(); - - account.setRegistrationLockPin(null, null); + context.getAccountHelper().removeRegistrationPin(); } } @@ -424,33 +312,9 @@ public class ManagerImpl implements Manager { return null; } - return new Group(groupInfo.getGroupId(), - groupInfo.getTitle(), - groupInfo.getDescription(), - groupInfo.getGroupInviteLink(), - groupInfo.getMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.getPendingMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.getRequestingMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.getAdminMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.isBlocked(), - groupInfo.getMessageExpirationTimer(), - groupInfo.getPermissionAddMember(), - groupInfo.getPermissionEditDetails(), - groupInfo.getPermissionSendMessage(), - groupInfo.isMember(account.getSelfRecipientId()), - groupInfo.isAdmin(account.getSelfRecipientId())); + return Group.from(groupInfo, + account.getRecipientStore()::resolveRecipientAddress, + account.getSelfRecipientId()); } @Override @@ -523,33 +387,28 @@ public class ManagerImpl implements Manager { try { final var recipientId = context.getRecipientHelper().resolveRecipient(single); final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId); - results.put(recipient, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + results.put(recipient, List.of(toSendMessageResult(result))); } catch (UnregisteredRecipientException e) { results.put(recipient, List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); } } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { final var result = context.getSendHelper().sendSelfMessage(messageBuilder); - results.put(recipient, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + results.put(recipient, List.of(toSendMessageResult(result))); } else if (recipient instanceof RecipientIdentifier.Group group) { final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId()); - results.put(recipient, - result.stream() - .map(sendMessageResult -> SendMessageResult.from(sendMessageResult, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress)) - .toList()); + results.put(recipient, result.stream().map(this::toSendMessageResult).toList()); } } return new SendMessageResults(timestamp, results); } + private SendMessageResult toSendMessageResult(final org.whispersystems.signalservice.api.messages.SendMessageResult result) { + return SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress); + } + private SendMessageResults sendTypingMessage( SignalServiceTypingMessage.Action action, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { @@ -561,10 +420,7 @@ public class ManagerImpl implements Manager { try { final var recipientId = context.getRecipientHelper().resolveRecipient(single); final var result = context.getSendHelper().sendTypingMessage(message, recipientId); - results.put(recipient, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + results.put(recipient, List.of(toSendMessageResult(result))); } catch (UnregisteredRecipientException e) { results.put(recipient, List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); @@ -573,12 +429,7 @@ public class ManagerImpl implements Manager { final var groupId = ((RecipientIdentifier.Group) recipient).groupId(); final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); final var result = context.getSendHelper().sendGroupTypingMessage(message, groupId); - results.put(recipient, - result.stream() - .map(r -> SendMessageResult.from(r, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress)) - .toList()); + results.put(recipient, result.stream().map(this::toSendMessageResult).toList()); } } return new SendMessageResults(timestamp, results); @@ -623,11 +474,7 @@ public class ManagerImpl implements Manager { try { final var result = context.getSendHelper() .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender)); - return new SendMessageResults(timestamp, - Map.of(sender, - List.of(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress)))); + return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result)))); } catch (UnregisteredRecipientException e) { return new SendMessageResults(timestamp, Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress())))); @@ -769,9 +616,6 @@ public class ManagerImpl implements Manager { context.getSyncHelper().sendBlockedList(); } - /** - * Change the expiration timer for a contact - */ @Override public void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer @@ -786,12 +630,6 @@ public class ManagerImpl implements Manager { } } - /** - * Upload the sticker pack from path. - * - * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file - * @return if successful, returns the URL to install the sticker pack in the signal app - */ @Override public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); @@ -1040,89 +878,44 @@ public class ManagerImpl implements Manager { return identity == null ? List.of() : List.of(toIdentity(identity)); } - /** - * Trust this the identity with this fingerprint - * - * @param recipient account of the identity - * @param fingerprint Fingerprint - */ @Override public boolean trustIdentityVerified( RecipientIdentifier.Single recipient, byte[] fingerprint ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - final var updated = context.getIdentityHelper().trustIdentityVerified(recipientId, fingerprint); - if (updated && this.isReceiving()) { - context.getReceiveHelper().setNeedsToRetryFailedMessages(true); - } - return updated; + return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityVerified(r, fingerprint)); } - /** - * Trust this the identity with this safety number - * - * @param recipient account of the identity - * @param safetyNumber Safety number - */ @Override public boolean trustIdentityVerifiedSafetyNumber( RecipientIdentifier.Single recipient, String safetyNumber ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); - if (updated && this.isReceiving()) { - context.getReceiveHelper().setNeedsToRetryFailedMessages(true); - } - return updated; + return trustIdentity(recipient, + r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber)); } - /** - * Trust this the identity with this scannable safety number - * - * @param recipient account of the identity - * @param safetyNumber Scannable safety number - */ @Override public boolean trustIdentityVerifiedSafetyNumber( RecipientIdentifier.Single recipient, byte[] safetyNumber ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); - if (updated && this.isReceiving()) { - context.getReceiveHelper().setNeedsToRetryFailedMessages(true); - } - return updated; + return trustIdentity(recipient, + r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber)); } - /** - * Trust all keys of this identity without verification - * - * @param recipient account of the identity - */ @Override public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { + return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityAllKeys(r)); + } + + private boolean trustIdentity( + RecipientIdentifier.Single recipient, Function trustMethod + ) throws UnregisteredRecipientException { RecipientId recipientId; try { recipientId = context.getRecipientHelper().resolveRecipient(recipient); } catch (IOException e) { return false; } - final var updated = context.getIdentityHelper().trustIdentityAllKeys(recipientId); + final var updated = trustMethod.apply(recipientId); if (updated && this.isReceiving()) { context.getReceiveHelper().setNeedsToRetryFailedMessages(true); } @@ -1137,7 +930,7 @@ public class ManagerImpl implements Manager { } @Override - public void close() throws IOException { + public void close() { Thread thread; synchronized (messageHandlers) { weakHandlers.clear(); diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Configuration.java b/lib/src/main/java/org/asamk/signal/manager/api/Configuration.java index 2ce132d3..7256ac64 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Configuration.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Configuration.java @@ -1,5 +1,7 @@ package org.asamk.signal.manager.api; +import org.asamk.signal.manager.storage.configuration.ConfigurationStore; + import java.util.Optional; public record Configuration( @@ -7,4 +9,12 @@ public record Configuration( Optional unidentifiedDeliveryIndicators, Optional typingIndicators, Optional linkPreviews -) {} +) { + + public static Configuration from(final ConfigurationStore configurationStore) { + return new Configuration(Optional.ofNullable(configurationStore.getReadReceipts()), + Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()), + Optional.ofNullable(configurationStore.getTypingIndicators()), + Optional.ofNullable(configurationStore.getLinkPreviews())); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Group.java b/lib/src/main/java/org/asamk/signal/manager/api/Group.java index 88be3539..a8dfdff1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Group.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java @@ -3,9 +3,13 @@ package org.asamk.signal.manager.api; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.storage.recipients.RecipientId; import java.util.Set; +import java.util.stream.Collectors; public record Group( GroupId groupId, @@ -23,4 +27,37 @@ public record Group( GroupPermission permissionSendMessage, boolean isMember, boolean isAdmin -) {} +) { + + public static Group from( + final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId + ) { + return new Group(groupInfo.getGroupId(), + groupInfo.getTitle(), + groupInfo.getDescription(), + groupInfo.getGroupInviteLink(), + groupInfo.getMembers() + .stream() + .map(recipientStore::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getPendingMembers() + .stream() + .map(recipientStore::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getRequestingMembers() + .stream() + .map(recipientStore::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getAdminMembers() + .stream() + .map(recipientStore::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.isBlocked(), + groupInfo.getMessageExpirationTimer(), + groupInfo.getPermissionAddMember(), + groupInfo.getPermissionEditDetails(), + groupInfo.getPermissionSendMessage(), + groupInfo.isMember(selfRecipientId), + groupInfo.isAdmin(selfRecipientId)); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java new file mode 100644 index 00000000..9c83c605 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -0,0 +1,152 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.DeviceLinkInfo; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.api.InvalidDeviceLinkException; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.api.util.DeviceNameUtil; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class AccountHelper { + + private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class); + + private final Context context; + private final SignalAccount account; + private final SignalDependencies dependencies; + + private Callable unregisteredListener; + + public AccountHelper(final Context context) { + this.account = context.getAccount(); + this.dependencies = context.getDependencies(); + this.context = context; + } + + public void setUnregisteredListener(final Callable unregisteredListener) { + this.unregisteredListener = unregisteredListener; + } + + public void checkAccountState() throws IOException { + if (account.getLastReceiveTimestamp() == 0) { + logger.info("The Signal protocol expects that incoming messages are regularly received."); + } else { + var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); + long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); + if (days > 7) { + logger.warn( + "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", + days); + } + } + try { + context.getPreKeyHelper().refreshPreKeysIfNecessary(); + if (account.getAci() == null) { + account.setAci(ACI.parseOrNull(dependencies.getAccountManager().getWhoAmI().getAci())); + } + updateAccountAttributes(); + } catch (AuthorizationFailedException e) { + account.setRegistered(false); + throw e; + } + } + + public void setDeviceName(String deviceName) { + final var privateKey = account.getIdentityKeyPair().getPrivateKey(); + final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + account.setEncryptedDeviceName(encryptedDeviceName); + } + + public void updateAccountAttributes() throws IOException { + dependencies.getAccountManager() + .setAccountAttributes(account.getEncryptedDeviceName(), + null, + account.getLocalRegistrationId(), + true, + null, + account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + ServiceConfig.capabilities, + account.isDiscoverableByPhoneNumber()); + } + + public void addDevice(DeviceLinkInfo deviceLinkInfo) throws IOException, InvalidDeviceLinkException { + var identityKeyPair = account.getIdentityKeyPair(); + var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); + + try { + dependencies.getAccountManager() + .addDevice(deviceLinkInfo.deviceIdentifier(), + deviceLinkInfo.deviceKey(), + identityKeyPair, + Optional.of(account.getProfileKey().serialize()), + verificationCode); + } catch (InvalidKeyException e) { + throw new InvalidDeviceLinkException("Invalid device link", e); + } + account.setMultiDevice(true); + } + + public void removeLinkedDevices(long deviceId) throws IOException { + dependencies.getAccountManager().removeDevice(deviceId); + var devices = dependencies.getAccountManager().getDevices(); + account.setMultiDevice(devices.size() > 1); + } + + public void setRegistrationPin(String pin) throws IOException { + final var masterKey = account.getPinMasterKey() != null + ? account.getPinMasterKey() + : KeyUtils.createMasterKey(); + + context.getPinHelper().setRegistrationLockPin(pin, masterKey); + + account.setRegistrationLockPin(pin, masterKey); + } + + public void removeRegistrationPin() throws IOException { + // Remove KBS Pin + context.getPinHelper().removeRegistrationLockPin(); + + account.setRegistrationLockPin(null, null); + } + + public void unregister() throws IOException { + // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. + // If this is the master device, other users can't send messages to this number anymore. + // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. + dependencies.getAccountManager().setGcmId(Optional.absent()); + + account.setRegistered(false); + unregisteredListener.call(); + } + + public void deleteAccount() throws IOException { + try { + context.getPinHelper().removeRegistrationLockPin(); + } catch (IOException e) { + logger.warn("Failed to remove registration lock pin"); + } + account.setRegistrationLockPin(null, null); + + dependencies.getAccountManager().deleteAccount(); + + account.setRegistered(false); + unregisteredListener.call(); + } + + public interface Callable { + + void call(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/Context.java b/lib/src/main/java/org/asamk/signal/manager/helper/Context.java index 79b7b959..f1b7c3c1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/Context.java @@ -20,6 +20,7 @@ public class Context { private final AttachmentStore attachmentStore; private final JobExecutor jobExecutor; + private AccountHelper accountHelper; private AttachmentHelper attachmentHelper; private ContactHelper contactHelper; private GroupHelper groupHelper; @@ -75,6 +76,10 @@ public class Context { return jobExecutor; } + public AccountHelper getAccountHelper() { + return getOrCreate(() -> accountHelper, () -> accountHelper = new AccountHelper(this)); + } + public AttachmentHelper getAttachmentHelper() { return getOrCreate(() -> attachmentHelper, () -> attachmentHelper = new AttachmentHelper(this)); } @@ -100,7 +105,7 @@ public class Context { () -> this.incomingMessageHandler = new IncomingMessageHandler(this)); } - public PinHelper getPinHelper() { + PinHelper getPinHelper() { return getOrCreate(() -> pinHelper, () -> pinHelper = new PinHelper(dependencies.getKeyBackupService())); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 8a8fdd3e..eac16e84 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -1034,13 +1034,17 @@ public class SignalAccount implements Closeable { } @Override - public void close() throws IOException { + public void close() { synchronized (fileChannel) { try { - lock.close(); - } catch (ClosedChannelException ignored) { + try { + lock.close(); + } catch (ClosedChannelException ignored) { + } + fileChannel.close(); + } catch (IOException e) { + logger.warn("Failed to close account: {}", e.getMessage(), e); } - fileChannel.close(); } } } diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index 58b6b80f..59478c52 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -5,7 +5,9 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.output.OutputWriter; import java.io.IOException; @@ -31,6 +33,8 @@ public class RemovePinCommand implements JsonRpcLocalCommand { m.setRegistrationLockPin(Optional.empty()); } catch (IOException e) { throw new IOErrorException("Remove pin error: " + e.getMessage(), e); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); } } } diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java index 53786554..3e4ef0b8 100644 --- a/src/main/java/org/asamk/signal/commands/SetPinCommand.java +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -5,7 +5,9 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.output.OutputWriter; import java.io.IOException; @@ -34,6 +36,8 @@ public class SetPinCommand implements JsonRpcLocalCommand { m.setRegistrationLockPin(Optional.of(registrationLockPin)); } catch (IOException e) { throw new IOErrorException("Set pin error: " + e.getMessage(), e); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); } } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index f22f08a6..003f6695 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -669,6 +669,8 @@ public class DbusSignalImpl implements Signal { m.setRegistrationLockPin(Optional.empty()); } catch (IOException e) { throw new Error.Failure("Remove pin error: " + e.getMessage()); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } } @@ -678,6 +680,8 @@ public class DbusSignalImpl implements Signal { m.setRegistrationLockPin(Optional.of(registrationLockPin)); } catch (IOException e) { throw new Error.Failure("Set pin error: " + e.getMessage()); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } }