diff --git a/bundles/org.openhab.binding.signal/pom.xml b/bundles/org.openhab.binding.signal/pom.xml index 7f29c57d4248d..c3e6a8d14ae8f 100644 --- a/bundles/org.openhab.binding.signal/pom.xml +++ b/bundles/org.openhab.binding.signal/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT org.openhab.binding.signal @@ -24,13 +24,13 @@ signal-service-java com.github.turasa - 2.15.3_unofficial_90 + 2.15.3_unofficial_96 compile com.github.turasa core-util-jvm - 2.15.3_unofficial_86 + 2.15.3_unofficial_96 com.squareup.wire @@ -41,7 +41,7 @@ org.signal libsignal-client - 0.36.1 + 0.39.2 compile diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/Manager.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/Manager.java index 41e08fb6309c9..f4d330a4cf13a 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/Manager.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/Manager.java @@ -34,6 +34,7 @@ import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.StickerPack; +import org.asamk.signal.manager.api.StickerPackId; import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.TypingAction; @@ -41,6 +42,7 @@ import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UserStatus; +import org.asamk.signal.manager.api.UsernameLinkUrl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -88,7 +90,12 @@ static boolean isSignalClientAvailable() { */ Map getUserStatus(Set numbers) throws IOException, RateLimitException; - void updateAccountAttributes(String deviceName) throws IOException; + void updateAccountAttributes( + String deviceName, + Boolean unrestrictedUnidentifiedSender, + final Boolean discoverableByNumber, + final Boolean numberSharing + ) throws IOException; Configuration getConfiguration(); @@ -100,11 +107,15 @@ static boolean isSignalClientAvailable() { */ void updateProfile(UpdateProfile updateProfile) throws IOException; + String getUsername(); + + UsernameLinkUrl getUsernameLink(); + /** * Set a username for the account. * If the username is null, it will be deleted. */ - String setUsername(String username) throws IOException, InvalidUsernameException; + void setUsername(String username) throws IOException, InvalidUsernameException; /** * Set a username for the account. @@ -167,7 +178,7 @@ SendMessageResults sendViewedReceipt( ); SendMessageResults sendMessage( - Message message, Set recipients + Message message, Set recipients, boolean notifySelf ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; SendMessageResults sendEditMessage( @@ -193,6 +204,12 @@ SendMessageResults sendPaymentNotificationMessage( SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; + SendMessageResults sendMessageRequestResponse( + MessageEnvelope.Sync.MessageRequestResponse.Type type, Set recipientIdentifiers + ); + + void hideRecipient(RecipientIdentifier.Single recipient); + void deleteRecipient(RecipientIdentifier.Single recipient); void deleteContact(RecipientIdentifier.Single recipient); @@ -255,6 +272,8 @@ void receiveMessages( Optional timeout, Optional maxMessages, ReceiveMessageHandler handler ) throws IOException, AlreadyReceivingException; + void stopReceiveMessages(); + void setReceiveConfig(ReceiveConfig receiveConfig); boolean isContactBlocked(RecipientIdentifier.Single recipient); @@ -298,6 +317,14 @@ boolean trustIdentityVerified( InputStream retrieveAttachment(final String id) throws IOException; + InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException; + + InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException; + + InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException; + + InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException; + @Override void close(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java deleted file mode 100644 index 8b29600685e53..0000000000000 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.asamk.signal.manager.actions; - -import org.asamk.signal.manager.helper.Context; - -public class RetrieveStorageDataAction implements HandleAction { - - private static final RetrieveStorageDataAction INSTANCE = new RetrieveStorageDataAction(); - - private RetrieveStorageDataAction() { - } - - public static RetrieveStorageDataAction create() { - return INSTANCE; - } - - @Override - public void execute(Context context) throws Throwable { - context.getStorageHelper().readDataFromStorage(); - } -} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/actions/SyncStorageDataAction.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/actions/SyncStorageDataAction.java new file mode 100644 index 0000000000000..7101b3d123fc3 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/actions/SyncStorageDataAction.java @@ -0,0 +1,21 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.helper.Context; +import org.asamk.signal.manager.jobs.SyncStorageJob; + +public class SyncStorageDataAction implements HandleAction { + + private static final SyncStorageDataAction INSTANCE = new SyncStorageDataAction(); + + private SyncStorageDataAction() { + } + + public static SyncStorageDataAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getJobExecutor().enqueueJob(new SyncStorageJob()); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/Contact.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/Contact.java index 27c2d4dae3d9e..cfe3f89492ffd 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/Contact.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/Contact.java @@ -2,50 +2,34 @@ import org.whispersystems.signalservice.internal.util.Util; -import java.util.Objects; - -public class Contact { - - private final String givenName; - - private final String familyName; - - private final String color; - - private final int messageExpirationTime; - - private final boolean blocked; - - private final boolean archived; - - private final boolean profileSharingEnabled; - - public Contact( - final String givenName, - final String familyName, - final String color, - final int messageExpirationTime, - final boolean blocked, - final boolean archived, - final boolean profileSharingEnabled - ) { - this.givenName = givenName; - this.familyName = familyName; - this.color = color; - this.messageExpirationTime = messageExpirationTime; - this.blocked = blocked; - this.archived = archived; - this.profileSharingEnabled = profileSharingEnabled; - } +public record Contact( + String givenName, + String familyName, + String nickName, + String color, + int messageExpirationTime, + long muteUntil, + boolean hideStory, + boolean isBlocked, + boolean isArchived, + boolean isProfileSharingEnabled, + boolean isHidden, + Long unregisteredTimestamp +) { private Contact(final Builder builder) { - givenName = builder.givenName; - familyName = builder.familyName; - color = builder.color; - messageExpirationTime = builder.messageExpirationTime; - blocked = builder.blocked; - archived = builder.archived; - profileSharingEnabled = builder.profileSharingEnabled; + this(builder.givenName, + builder.familyName, + builder.nickName, + builder.color, + builder.messageExpirationTime, + builder.muteUntil, + builder.hideStory, + builder.isBlocked, + builder.isArchived, + builder.isProfileSharingEnabled, + builder.isHidden, + builder.unregisteredTimestamp); } public static Builder newBuilder() { @@ -54,13 +38,18 @@ public static Builder newBuilder() { public static Builder newBuilder(final Contact copy) { Builder builder = new Builder(); - builder.givenName = copy.getGivenName(); - builder.familyName = copy.getFamilyName(); - builder.color = copy.getColor(); - builder.messageExpirationTime = copy.getMessageExpirationTime(); - builder.blocked = copy.isBlocked(); - builder.archived = copy.isArchived(); - builder.profileSharingEnabled = copy.isProfileSharingEnabled(); + builder.givenName = copy.givenName(); + builder.familyName = copy.familyName(); + builder.nickName = copy.nickName(); + builder.color = copy.color(); + builder.messageExpirationTime = copy.messageExpirationTime(); + builder.muteUntil = copy.muteUntil(); + builder.hideStory = copy.hideStory(); + builder.isBlocked = copy.isBlocked(); + builder.isArchived = copy.isArchived(); + builder.isProfileSharingEnabled = copy.isProfileSharingEnabled(); + builder.isHidden = copy.isHidden(); + builder.unregisteredTimestamp = copy.unregisteredTimestamp(); return builder; } @@ -79,72 +68,28 @@ public String getName() { return givenName + " " + familyName; } - public String getGivenName() { - return givenName; - } - - public String getFamilyName() { - return familyName; - } - - public String getColor() { - return color; - } - - public int getMessageExpirationTime() { - return messageExpirationTime; - } - - public boolean isBlocked() { - return blocked; - } - - public boolean isArchived() { - return archived; - } - - public boolean isProfileSharingEnabled() { - return profileSharingEnabled; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final Contact contact = (Contact) o; - return messageExpirationTime == contact.messageExpirationTime - && blocked == contact.blocked - && archived == contact.archived - && profileSharingEnabled == contact.profileSharingEnabled - && Objects.equals(givenName, contact.givenName) - && Objects.equals(familyName, contact.familyName) - && Objects.equals(color, contact.color); - } - - @Override - public int hashCode() { - return Objects.hash(givenName, - familyName, - color, - messageExpirationTime, - blocked, - archived, - profileSharingEnabled); - } - public static final class Builder { private String givenName; private String familyName; + private String nickName; private String color; private int messageExpirationTime; - private boolean blocked; - private boolean archived; - private boolean profileSharingEnabled; + private long muteUntil; + private boolean hideStory; + private boolean isBlocked; + private boolean isArchived; + private boolean isProfileSharingEnabled; + private boolean isHidden; + private Long unregisteredTimestamp; private Builder() { } + public static Builder newBuilder() { + return new Builder(); + } + public Builder withGivenName(final String val) { givenName = val; return this; @@ -155,6 +100,11 @@ public Builder withFamilyName(final String val) { return this; } + public Builder withNickName(final String val) { + nickName = val; + return this; + } + public Builder withColor(final String val) { color = val; return this; @@ -165,18 +115,38 @@ public Builder withMessageExpirationTime(final int val) { return this; } - public Builder withBlocked(final boolean val) { - blocked = val; + public Builder withMuteUntil(final long val) { + muteUntil = val; + return this; + } + + public Builder withHideStory(final boolean val) { + hideStory = val; + return this; + } + + public Builder withIsBlocked(final boolean val) { + isBlocked = val; + return this; + } + + public Builder withIsArchived(final boolean val) { + isArchived = val; + return this; + } + + public Builder withIsProfileSharingEnabled(final boolean val) { + isProfileSharingEnabled = val; return this; } - public Builder withArchived(final boolean val) { - archived = val; + public Builder withIsHidden(final boolean val) { + isHidden = val; return this; } - public Builder withProfileSharingEnabled(final boolean val) { - profileSharingEnabled = val; + public Builder withUnregisteredTimestamp(final Long val) { + unregisteredTimestamp = val; return this; } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/GroupInviteLinkUrl.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/GroupInviteLinkUrl.java index 0c00b3096ff46..a5a23e5613665 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/GroupInviteLinkUrl.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/GroupInviteLinkUrl.java @@ -112,7 +112,7 @@ public GroupLinkPassword getPassword() { return password; } - public final static class InvalidGroupLinkException extends Exception { + public static final class InvalidGroupLinkException extends Exception { public InvalidGroupLinkException(String message) { super(message); @@ -123,7 +123,7 @@ public InvalidGroupLinkException(Throwable cause) { } } - public final static class UnknownGroupLinkVersionException extends Exception { + public static final class UnknownGroupLinkVersionException extends Exception { public UnknownGroupLinkVersionException(String message) { super(message); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/MessageEnvelope.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/MessageEnvelope.java index dc157fe60a814..c5da641c59134 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -481,7 +481,7 @@ public record Address( ) { static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) { - return new Address(Address.Type.from(address.getType()), + return new Address(Type.from(address.getType()), address.getLabel(), address.getStreet(), address.getPobox(), @@ -690,7 +690,9 @@ public enum Type { DELETE, BLOCK, BLOCK_AND_DELETE, - UNBLOCK_AND_ACCEPT; + UNBLOCK_AND_ACCEPT, + SPAM, + BLOCK_AND_SPAM; static Type from(MessageRequestResponseMessage.Type type) { return switch (type) { @@ -700,6 +702,8 @@ static Type from(MessageRequestResponseMessage.Type type) { case BLOCK -> BLOCK; case BLOCK_AND_DELETE -> BLOCK_AND_DELETE; case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT; + case SPAM -> SPAM; + case BLOCK_AND_SPAM -> BLOCK_AND_SPAM; }; } } @@ -716,7 +720,6 @@ public record Call( Optional busy, List iceUpdate, Optional opaque, - boolean isMultiRing, boolean isUrgent ) { @@ -732,17 +735,13 @@ public static Call from(final SignalServiceCallMessage callMessage) { .map(m -> m.stream().map(IceUpdate::from).toList()) .orElse(List.of()), callMessage.getOpaqueMessage().map(Opaque::from), - callMessage.isMultiRing(), callMessage.isUrgent()); } - public record Offer(long id, String sdp, Type type, byte[] opaque) { + public record Offer(long id, Type type, byte[] opaque) { static Offer from(OfferMessage offerMessage) { - return new Offer(offerMessage.getId(), - offerMessage.getSdp(), - Type.from(offerMessage.getType()), - offerMessage.getOpaque()); + return new Offer(offerMessage.getId(), Type.from(offerMessage.getType()), offerMessage.getOpaque()); } public enum Type { @@ -758,10 +757,10 @@ static Type from(OfferMessage.Type type) { } } - public record Answer(long id, String sdp, byte[] opaque) { + public record Answer(long id, byte[] opaque) { static Answer from(AnswerMessage answerMessage) { - return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque()); + return new Answer(answerMessage.getId(), answerMessage.getOpaque()); } } @@ -799,10 +798,10 @@ static Type from(HangupMessage.Type type) { } } - public record IceUpdate(long id, String sdp, byte[] opaque) { + public record IceUpdate(long id, byte[] opaque) { static IceUpdate from(IceUpdateMessage iceUpdateMessage) { - return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque()); + return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getOpaque()); } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/RecipientAddress.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/RecipientAddress.java index f042f8fbab8ce..c94a21acb4756 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/RecipientAddress.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/RecipientAddress.java @@ -19,7 +19,7 @@ public record RecipientAddress(Optional uuid, Optional number, Opt public RecipientAddress { uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid; if (uuid.isEmpty() && number.isEmpty() && username.isEmpty()) { - throw new AssertionError("Must have either a UUID or E164 number!"); + throw new AssertionError("Must have either a UUID, username or E164 number!"); } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackId.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackId.java index 38ea495011e23..e6b5a957eb2a6 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackId.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackId.java @@ -1,5 +1,7 @@ package org.asamk.signal.manager.api; +import org.whispersystems.signalservice.internal.util.Hex; + import java.util.Arrays; public class StickerPackId { @@ -32,4 +34,9 @@ public boolean equals(final Object o) { public int hashCode() { return Arrays.hashCode(id); } + + @Override + public String toString() { + return "StickerPackId{" + Hex.toStringCondensed(id) + '}'; + } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackUrl.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackUrl.java index 7113b4b8b4ac7..ea4e3f3eb6ff5 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackUrl.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/StickerPackUrl.java @@ -59,7 +59,7 @@ public URI getUrl() { } } - public final static class InvalidStickerPackLinkException extends Exception { + public static final class InvalidStickerPackLinkException extends Exception { public InvalidStickerPackLinkException(String message) { super(message); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/TrustLevel.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/TrustLevel.java index cbfa0bd52447b..ac93e34a6cae8 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/TrustLevel.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/TrustLevel.java @@ -1,7 +1,6 @@ package org.asamk.signal.manager.api; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; -import org.whispersystems.signalservice.internal.storage.protos.ContactRecord; public enum TrustLevel { UNTRUSTED, @@ -17,14 +16,6 @@ public static TrustLevel fromInt(int i) { return TrustLevel.cachedValues[i]; } - public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) { - return switch (identityState) { - case DEFAULT -> TRUSTED_UNVERIFIED; - case UNVERIFIED -> UNTRUSTED; - case VERIFIED -> TRUSTED_VERIFIED; - }; - } - public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { return switch (verifiedState) { case DEFAULT -> TRUSTED_UNVERIFIED; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/UsernameLinkUrl.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/UsernameLinkUrl.java new file mode 100644 index 0000000000000..f8fff6ebf8936 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/api/UsernameLinkUrl.java @@ -0,0 +1,82 @@ +package org.asamk.signal.manager.api; + +import org.signal.core.util.Base64; +import org.whispersystems.signalservice.api.push.UsernameLinkComponents; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.IOException; +import java.util.Arrays; +import java.util.regex.Pattern; + +public final class UsernameLinkUrl { + + private static final Pattern URL_REGEX = Pattern.compile("(https://)?signal.me/?#eu/([a-zA-Z0-9+\\-_/]+)"); + + private static final String BASE_URL = "https://signal.me/#eu/"; + + private final String url; + private final UsernameLinkComponents usernameLinkComponents; + + public static UsernameLinkUrl fromUri(String url) throws InvalidUsernameLinkException { + final var matcher = URL_REGEX.matcher(url); + if (!matcher.matches()) { + throw new InvalidUsernameLinkException("Invalid username link"); + } + final var path = matcher.group(2); + final byte[] allBytes; + try { + allBytes = Base64.decode(path); + } catch (IOException e) { + throw new InvalidUsernameLinkException("Invalid base64 encoding"); + } + + if (allBytes.length != 48) { + throw new InvalidUsernameLinkException("Invalid username link"); + } + + final var entropy = Arrays.copyOfRange(allBytes, 0, 32); + final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length); + final var serverIdUuid = UuidUtil.parseOrNull(serverId); + if (serverIdUuid == null) { + throw new InvalidUsernameLinkException("Invalid serverId"); + } + + return new UsernameLinkUrl(new UsernameLinkComponents(entropy, serverIdUuid)); + } + + public UsernameLinkUrl(UsernameLinkComponents usernameLinkComponents) { + this.usernameLinkComponents = usernameLinkComponents; + this.url = createUrl(usernameLinkComponents); + } + + private static String createUrl(UsernameLinkComponents usernameLinkComponents) { + final var entropy = usernameLinkComponents.getEntropy(); + final var serverId = UuidUtil.toByteArray(usernameLinkComponents.getServerId()); + + final var combined = new byte[entropy.length + serverId.length]; + System.arraycopy(entropy, 0, combined, 0, entropy.length); + System.arraycopy(serverId, 0, combined, entropy.length, serverId.length); + + final var base64 = Base64.encodeUrlSafeWithoutPadding(combined); + return BASE_URL + base64; + } + + public String getUrl() { + return url; + } + + public UsernameLinkComponents getComponents() { + return usernameLinkComponents; + } + + public static final class InvalidUsernameLinkException extends Exception { + + public InvalidUsernameLinkException(String message) { + super(message); + } + + public InvalidUsernameLinkException(Throwable cause) { + super(cause); + } + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/LiveConfig.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/LiveConfig.java index 2756f292922a5..3c477299b75df 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/LiveConfig.java @@ -20,29 +20,35 @@ import okhttp3.Dns; import okhttp3.Interceptor; +import static org.asamk.signal.manager.api.ServiceEnvironment.LIVE; + class LiveConfig { - private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() + private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); - private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private final static String SVR2_MRENCLAVE = "6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094"; + private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; + private static final String SVR2_MRENCLAVE = "a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97"; + private static final String SVR2_MRENCLAVE_DEPRECATED = "6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094"; - private final static String URL = "https://chat.signal.org"; - private final static String CDN_URL = "https://cdn.signal.org"; - private final static String CDN2_URL = "https://cdn2.signal.org"; - private final static String STORAGE_URL = "https://storage.signal.org"; - private final static String SIGNAL_CDSI_URL = "https://cdsi.signal.org"; - private final static String SIGNAL_SVR2_URL = "https://svr2.signal.org"; - private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + private static final String URL = "https://chat.signal.org"; + private static final String CDN_URL = "https://cdn.signal.org"; + private static final String CDN2_URL = "https://cdn2.signal.org"; + private static final String STORAGE_URL = "https://storage.signal.org"; + private static final String SIGNAL_CDSI_URL = "https://cdsi.signal.org"; + private static final String SIGNAL_SVR2_URL = "https://svr2.signal.org"; + private static final TrustStore TRUST_STORE = new WhisperTrustStore(); - private final static Optional dns = Optional.empty(); - private final static Optional proxy = Optional.empty(); + private static final Optional dns = Optional.empty(); + private static final Optional proxy = Optional.empty(); - private final static byte[] zkGroupServerPublicParams = Base64.getDecoder() - .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P"); - private final static byte[] genericServerPublicParams = Base64.getDecoder() + private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() + .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I="); + private static final byte[] genericServerPublicParams = Base64.getDecoder() .decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN"); + private static final byte[] backupServerPublicParams = Base64.getDecoder() + .decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O"); + static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors ) { @@ -58,7 +64,8 @@ static SignalServiceConfiguration createDefaultServiceConfiguration( dns, proxy, zkGroupServerPublicParams, - genericServerPublicParams); + genericServerPublicParams, + backupServerPublicParams); } static ECPublicKey getUnidentifiedSenderTrustRoot() { @@ -69,12 +76,12 @@ static ECPublicKey getUnidentifiedSenderTrustRoot() { } } - static String getCdsiMrenclave() { - return CDSI_MRENCLAVE; - } - - static String getSvr2Mrenclave() { - return SVR2_MRENCLAVE; + static ServiceEnvironmentConfig getServiceEnvironmentConfig(List interceptors) { + return new ServiceEnvironmentConfig(LIVE, + createDefaultServiceConfiguration(interceptors), + getUnidentifiedSenderTrustRoot(), + CDSI_MRENCLAVE, + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_DEPRECATED)); } private LiveConfig() { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceConfig.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceConfig.java index c37dc18b61492..ea47b8a6797f3 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -11,25 +11,26 @@ public class ServiceConfig { - public final static int PREKEY_MINIMUM_COUNT = 10; - public final static int PREKEY_BATCH_SIZE = 100; - public final static int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE; + public static final int PREKEY_MINIMUM_COUNT = 10; + public static final int PREKEY_BATCH_SIZE = 100; + public static final int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE; public static final long PREKEY_ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30); public static final long PREKEY_STALE_AGE = TimeUnit.DAYS.toMillis(90); public static final long SIGNED_PREKEY_ROTATE_AGE = TimeUnit.DAYS.toMillis(2); - public final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; - public final static long MAX_ENVELOPE_SIZE = 0; - public final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; - public final static boolean AUTOMATIC_NETWORK_RETRY = true; - public final static int GROUP_MAX_SIZE = 1001; - public final static int MAXIMUM_ONE_OFF_REQUEST_SIZE = 3; + public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; + public static final long MAX_ENVELOPE_SIZE = 0; + public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; + public static final boolean AUTOMATIC_NETWORK_RETRY = true; + public static final int GROUP_MAX_SIZE = 1001; + public static final int MAXIMUM_ONE_OFF_REQUEST_SIZE = 3; + public static final long UNREGISTERED_LIFESPAN = TimeUnit.DAYS.toMillis(30); public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { final var giftBadges = !isPrimaryDevice; final var pni = !isPrimaryDevice; final var paymentActivation = !isPrimaryDevice; - return new AccountAttributes.Capabilities(false, true, true, true, true, giftBadges, pni, paymentActivation); + return new AccountAttributes.Capabilities(true, true, true, true, true, giftBadges, pni, paymentActivation); } public static ServiceEnvironmentConfig getServiceEnvironmentConfig( @@ -43,16 +44,8 @@ public static ServiceEnvironmentConfig getServiceEnvironmentConfig( final var interceptors = List.of(userAgentInterceptor); return switch (serviceEnvironment) { - case LIVE -> new ServiceEnvironmentConfig(serviceEnvironment, - LiveConfig.createDefaultServiceConfiguration(interceptors), - LiveConfig.getUnidentifiedSenderTrustRoot(), - LiveConfig.getCdsiMrenclave(), - LiveConfig.getSvr2Mrenclave()); - case STAGING -> new ServiceEnvironmentConfig(serviceEnvironment, - StagingConfig.createDefaultServiceConfiguration(interceptors), - StagingConfig.getUnidentifiedSenderTrustRoot(), - StagingConfig.getCdsiMrenclave(), - StagingConfig.getSvr2Mrenclave()); + case LIVE -> LiveConfig.getServiceEnvironmentConfig(interceptors); + case STAGING -> StagingConfig.getServiceEnvironmentConfig(interceptors); }; } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java index 9664dcae0ab96..f4622064c939d 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java @@ -4,10 +4,12 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import java.util.List; + public record ServiceEnvironmentConfig( ServiceEnvironment type, SignalServiceConfiguration signalServiceConfiguration, ECPublicKey unidentifiedSenderTrustRoot, String cdsiMrenclave, - String svr2Mrenclave + List svr2Mrenclaves ) {} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/StagingConfig.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/StagingConfig.java index af1963035a4c6..83bb7be60f5d8 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/StagingConfig.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/config/StagingConfig.java @@ -20,29 +20,35 @@ import okhttp3.Dns; import okhttp3.Interceptor; +import static org.asamk.signal.manager.api.ServiceEnvironment.STAGING; + class StagingConfig { - private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() + private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() .decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"); - private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private final static String SVR2_MRENCLAVE = "a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95"; + private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; + private static final String SVR2_MRENCLAVE = "acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482"; + private static final String SVR2_MRENCLAVE_DEPRECATED = "a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95"; - private final static String URL = "https://chat.staging.signal.org"; - private final static String CDN_URL = "https://cdn-staging.signal.org"; - private final static String CDN2_URL = "https://cdn2-staging.signal.org"; - private final static String STORAGE_URL = "https://storage-staging.signal.org"; - private final static String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org"; - private final static String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org"; - private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + private static final String URL = "https://chat.staging.signal.org"; + private static final String CDN_URL = "https://cdn-staging.signal.org"; + private static final String CDN2_URL = "https://cdn2-staging.signal.org"; + private static final String STORAGE_URL = "https://storage-staging.signal.org"; + private static final String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org"; + private static final String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org"; + private static final TrustStore TRUST_STORE = new WhisperTrustStore(); - private final static Optional dns = Optional.empty(); - private final static Optional proxy = Optional.empty(); + private static final Optional dns = Optional.empty(); + private static final Optional proxy = Optional.empty(); - private final static byte[] zkGroupServerPublicParams = Base64.getDecoder() - .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj"); - private final static byte[] genericServerPublicParams = Base64.getDecoder() + private static final byte[] zkGroupServerPublicParams = Base64.getDecoder() + .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM="); + private static final byte[] genericServerPublicParams = Base64.getDecoder() .decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N"); + private static final byte[] backupServerPublicParams = Base64.getDecoder() + .decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8"); + static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors ) { @@ -58,7 +64,8 @@ static SignalServiceConfiguration createDefaultServiceConfiguration( dns, proxy, zkGroupServerPublicParams, - genericServerPublicParams); + genericServerPublicParams, + backupServerPublicParams); } static ECPublicKey getUnidentifiedSenderTrustRoot() { @@ -69,12 +76,12 @@ static ECPublicKey getUnidentifiedSenderTrustRoot() { } } - static String getCdsiMrenclave() { - return CDSI_MRENCLAVE; - } - - static String getSvr2Mrenclave() { - return SVR2_MRENCLAVE; + static ServiceEnvironmentConfig getServiceEnvironmentConfig(List interceptors) { + return new ServiceEnvironmentConfig(STAGING, + createDefaultServiceConfiguration(interceptors), + getUnidentifiedSenderTrustRoot(), + CDSI_MRENCLAVE, + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_DEPRECATED)); } private StagingConfig() { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AccountHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AccountHelper.java index d143c6316b9ac..1f44e2a36fe46 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.internal.SignalDependencies; +import org.asamk.signal.manager.jobs.SyncStorageJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.NumberVerificationUtils; @@ -33,6 +34,9 @@ import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; +import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException; +import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException; +import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; @@ -54,7 +58,7 @@ public class AccountHelper { - private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class); + private static final Logger logger = LoggerFactory.getLogger(AccountHelper.class); private final Context context; private final SignalAccount account; @@ -86,7 +90,11 @@ public void checkAccountState() throws IOException { } try { updateAccountAttributes(); - context.getPreKeyHelper().refreshPreKeysIfNecessary(); + if (account.getPreviousStorageVersion() < 9) { + context.getPreKeyHelper().forceRefreshPreKeys(); + } else { + context.getPreKeyHelper().refreshPreKeysIfNecessary(); + } if (account.getAci() == null || account.getPni() == null) { checkWhoAmiI(); } @@ -98,6 +106,13 @@ public void checkAccountState() throws IOException { && account.getRegistrationLockPin() != null) { migrateRegistrationPin(); } + if (account.getUsername() != null && account.getUsernameLink() == null) { + try { + tryToSetUsernameLink(new Username(account.getUsername())); + } catch (BaseUsernameException e) { + logger.debug("Invalid local username"); + } + } } catch (DeprecatedVersionException e) { logger.debug("Signal-Server returned deprecated version exception", e); throw e; @@ -127,11 +142,11 @@ private void updateSelfIdentifiers(final String number, final ACI aci, final PNI account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair()); } account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); - // TODO check and update remote storage context.getUnidentifiedAccessHelper().rotateSenderCertificates(); dependencies.resetAfterAddressChange(); context.getGroupV2Helper().clearAuthCredentialCache(); context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci()); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); } public void setPni( @@ -305,13 +320,17 @@ public void handlePniChangeNumberMessage( public static final int USERNAME_MIN_LENGTH = 3; public static final int USERNAME_MAX_LENGTH = 32; - public String reserveUsername(String nickname) throws IOException, BaseUsernameException { + public void reserveUsername(String nickname) throws IOException, BaseUsernameException { final var currentUsername = account.getUsername(); if (currentUsername != null) { final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.')); if (currentNickname.equals(nickname)) { - refreshCurrentUsername(); - return currentUsername; + try { + refreshCurrentUsername(); + } catch (IOException | BaseUsernameException e) { + logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username"); + } + return; } } @@ -329,14 +348,13 @@ public String reserveUsername(String nickname) throws IOException, BaseUsernameE } logger.debug("[reserveUsername] Successfully reserved username."); - final var username = candidates.get(hashIndex).getUsername(); + final var username = candidates.get(hashIndex); - dependencies.getAccountManager().confirmUsername(username, response); - account.setUsername(username); + final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); + account.setUsername(username.getUsername()); + account.setUsernameLink(linkComponents); account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); logger.debug("[confirmUsername] Successfully confirmed username."); - - return username; } public void refreshCurrentUsername() throws IOException, BaseUsernameException { @@ -348,7 +366,8 @@ public void refreshCurrentUsername() throws IOException, BaseUsernameException { final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI(); final var serverUsernameHash = whoAmIResponse.getUsernameHash(); final var hasServerUsername = !isEmpty(serverUsernameHash); - final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(new Username(localUsername).getHash()); + final var username = new Username(localUsername); + final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash()); if (!hasServerUsername) { logger.debug("No remote username is set."); @@ -360,17 +379,48 @@ public void refreshCurrentUsername() throws IOException, BaseUsernameException { if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) { logger.debug("Attempting to resynchronize username."); - tryReserveConfirmUsername(localUsername, localUsernameHash); + try { + tryReserveConfirmUsername(username); + } catch (UsernameMalformedException | UsernameTakenException | UsernameIsNotReservedException e) { + logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})", + e.getMessage(), + e.getClass().getSimpleName()); + account.setUsername(null); + account.setUsernameLink(null); + throw e; + } } else { logger.debug("Username already set, not refreshing."); } } - private void tryReserveConfirmUsername(final String username, String localUsernameHash) throws IOException { - final var response = dependencies.getAccountManager().reserveUsername(List.of(localUsernameHash)); - logger.debug("[reserveUsername] Successfully reserved existing username."); - dependencies.getAccountManager().confirmUsername(username, response); - logger.debug("[confirmUsername] Successfully confirmed existing username."); + private void tryReserveConfirmUsername(final Username username) throws IOException { + final var usernameLink = account.getUsernameLink(); + + if (usernameLink == null) { + dependencies.getAccountManager() + .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))); + logger.debug("[reserveUsername] Successfully reserved existing username."); + final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username); + account.setUsernameLink(linkComponents); + logger.debug("[confirmUsername] Successfully confirmed existing username."); + } else { + final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink); + account.setUsernameLink(linkComponents); + logger.debug("[confirmUsername] Successfully reclaimed existing username and link."); + } + } + + private void tryToSetUsernameLink(Username username) { + for (var i = 1; i < 4; i++) { + try { + final var linkComponents = dependencies.getAccountManager().createUsernameLink(username); + account.setUsernameLink(linkComponents); + break; + } catch (IOException e) { + logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e); + } + } } public void deleteUsername() throws IOException { @@ -405,6 +455,7 @@ public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidD throw new InvalidDeviceLinkException("Invalid device link", e); } account.setMultiDevice(true); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); } public void removeLinkedDevices(int deviceId) throws IOException { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AttachmentHelper.java index 7c2d908940411..75f0fd4a26821 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -25,7 +25,7 @@ public class AttachmentHelper { - private final static Logger logger = LoggerFactory.getLogger(AttachmentHelper.class); + private static final Logger logger = LoggerFactory.getLogger(AttachmentHelper.class); private final SignalDependencies dependencies; private final AttachmentStore attachmentStore; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ContactHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ContactHelper.java index 80df94478338e..f9af27f18cf96 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ContactHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ContactHelper.java @@ -20,6 +20,7 @@ public boolean isContactBlocked(final RecipientId recipientId) { public void setContactName(final RecipientId recipientId, final String givenName, final String familyName) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + builder.withIsHidden(false); if (givenName != null) { builder.withGivenName(givenName); } @@ -31,7 +32,7 @@ public void setContactName(final RecipientId recipientId, final String givenName public void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { var contact = account.getContactStore().getContact(recipientId); - if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + if (contact != null && contact.messageExpirationTime() == messageExpirationTimer) { return; } final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); @@ -43,8 +44,21 @@ public void setContactBlocked(RecipientId recipientId, boolean blocked) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); if (blocked) { - builder.withProfileSharingEnabled(false); + builder.withIsProfileSharingEnabled(false); } - account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); + account.getContactStore().storeContact(recipientId, builder.withIsBlocked(blocked).build()); + } + + public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + builder.withIsProfileSharingEnabled(profileSharing); + account.getContactStore().storeContact(recipientId, builder.build()); + } + + public void setContactHidden(RecipientId recipientId, boolean hidden) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withIsHidden(hidden).build()); } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/Context.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/Context.java index e5824a5af26a4..848a57c161390 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/Context.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/Context.java @@ -9,7 +9,7 @@ import java.util.function.Supplier; -public class Context { +public class Context implements AutoCloseable { private final Object LOCK = new Object(); @@ -80,7 +80,7 @@ AttachmentStore getAttachmentStore() { return attachmentStore; } - JobExecutor getJobExecutor() { + public JobExecutor getJobExecutor() { return jobExecutor; } @@ -170,6 +170,11 @@ private T getOrCreate(Supplier supplier, Callable creator) { } } + @Override + public void close() { + jobExecutor.close(); + } + private interface Callable { void call(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupHelper.java index 5991202e5e43f..75958defb461b 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -19,6 +19,7 @@ import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.internal.SignalDependencies; +import org.asamk.signal.manager.jobs.SyncStorageJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; @@ -62,7 +63,7 @@ public class GroupHelper { - private final static Logger logger = LoggerFactory.getLogger(GroupHelper.class); + private static final Logger logger = LoggerFactory.getLogger(GroupHelper.class); private final SignalAccount account; private final SignalDependencies dependencies; @@ -107,23 +108,8 @@ public GroupInfoV2 getOrMigrateGroup( ) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - var groupId = GroupUtils.getGroupIdV2(groupSecretParams); - var groupInfo = getGroup(groupId); - final GroupInfoV2 groupInfoV2; - if (groupInfo instanceof GroupInfoV1) { - // Received a v2 group message for a v1 group, we need to locally migrate the group - account.getGroupStore().deleteGroup(((GroupInfoV1) groupInfo).getGroupId()); - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, account.getRecipientResolver()); - groupInfoV2.setBlocked(groupInfo.isBlocked()); - account.getGroupStore().updateGroup(groupInfoV2); - logger.info("Locally migrated group {} to group v2, id: {}", - groupInfo.getGroupId().toBase64(), - groupInfoV2.getGroupId().toBase64()); - } else if (groupInfo instanceof GroupInfoV2) { - groupInfoV2 = (GroupInfoV2) groupInfo; - } else { - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, account.getRecipientResolver()); - } + final var groupId = GroupUtils.getGroupIdV2(groupSecretParams); + final var groupInfoV2 = account.getGroupStore().getGroupOrPartialMigrate(groupMasterKey, groupId); if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().revision < revision) { DecryptedGroup group = null; @@ -158,6 +144,7 @@ public GroupInfoV2 getOrMigrateGroup( } groupInfoV2.setGroup(group); account.getGroupStore().updateGroup(groupInfoV2); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); } return groupInfoV2; @@ -179,6 +166,7 @@ public Pair createGroup( if (gv2Pair == null) { // Failed to create v2 group, creating v1 group instead var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); + gv1.setProfileSharingEnabled(true); gv1.addMembers(List.of(selfRecipientId)); final var result = updateGroupV1(gv1, name, members, avatarBytes); return new Pair<>(gv1.getGroupId(), result); @@ -188,6 +176,7 @@ public Pair createGroup( final var decryptedGroup = gv2Pair.second(); gv2.setGroup(decryptedGroup); + gv2.setProfileSharingEnabled(true); if (avatarBytes != null) { context.getAvatarStore() .storeGroupAvatar(gv2.getGroupId(), outputStream -> outputStream.write(avatarBytes)); @@ -200,6 +189,7 @@ public Pair createGroup( final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId), gv2.getDistributionId()); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); return new Pair<>(gv2.getGroupId(), result); } @@ -224,9 +214,10 @@ public SendGroupMessageResults updateGroup( var group = getGroupForUpdating(groupId); final var avatarBytes = readAvatarBytes(avatarFile); - if (group instanceof GroupInfoV2) { + SendGroupMessageResults results; + if (group instanceof GroupInfoV2 gv2) { try { - return updateGroupV2((GroupInfoV2) group, + results = updateGroupV2(gv2, name, description, members, @@ -245,7 +236,7 @@ public SendGroupMessageResults updateGroup( } catch (ConflictException e) { // Detected conflicting update, refreshing group and trying again group = getGroup(groupId, true); - return updateGroupV2((GroupInfoV2) group, + results = updateGroupV2((GroupInfoV2) group, name, description, members, @@ -262,14 +253,15 @@ public SendGroupMessageResults updateGroup( expirationTimer, isAnnouncementGroup); } + } else { + GroupInfoV1 gv1 = (GroupInfoV1) group; + results = updateGroupV1(gv1, name, members, avatarBytes); + if (expirationTimer != null) { + setExpirationTimer(gv1, expirationTimer); + } } - - final var gv1 = (GroupInfoV1) group; - final var result = updateGroupV1(gv1, name, members, avatarBytes); - if (expirationTimer != null) { - setExpirationTimer(gv1, expirationTimer); - } - return result; + context.getJobExecutor().enqueueJob(new SyncStorageJob()); + return results; } public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException { @@ -316,6 +308,7 @@ public Pair joinGroup( final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); return new Pair<>(group.getGroupId(), result); } @@ -339,6 +332,7 @@ public SendGroupMessageResults quitGroup( public void deleteGroup(GroupId groupId) throws IOException { account.getGroupStore().deleteGroup(groupId); context.getAvatarStore().deleteGroupAvatar(groupId); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); } public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { @@ -349,6 +343,7 @@ public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws group.setBlocked(blocked); account.getGroupStore().updateGroup(group); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); } public SendGroupMessageResults sendGroupInfoRequest( @@ -563,7 +558,7 @@ private void setExpirationTimer( private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId, Optional.empty()); + context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId, false, Optional.empty()); } private SendGroupMessageResults updateGroupV2( diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 96ecf2c076e12..83f4992610bad 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -59,7 +59,7 @@ class GroupV2Helper { - private final static Logger logger = LoggerFactory.getLogger(GroupV2Helper.class); + private static final Logger logger = LoggerFactory.getLogger(GroupV2Helper.class); private final SignalDependencies dependencies; private final Context context; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IdentityHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IdentityHelper.java index 975d517923e6e..cc71e15c21b30 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IdentityHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IdentityHelper.java @@ -19,7 +19,7 @@ public class IdentityHelper { - private final static Logger logger = LoggerFactory.getLogger(IdentityHelper.class); + private static final Logger logger = LoggerFactory.getLogger(IdentityHelper.class); private final SignalAccount account; private final Context context; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 13efed5875a36..f7b978ab588d4 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -6,7 +6,6 @@ import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.ResendMessageAction; import org.asamk.signal.manager.actions.RetrieveProfileAction; -import org.asamk.signal.manager.actions.RetrieveStorageDataAction; import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendProfileKeyAction; @@ -17,6 +16,7 @@ import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; import org.asamk.signal.manager.actions.SendSyncKeysAction; +import org.asamk.signal.manager.actions.SyncStorageDataAction; import org.asamk.signal.manager.actions.UpdateAccountAttributesAction; import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupNotFoundException; @@ -64,6 +64,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; +import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.Envelope; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; @@ -75,7 +76,7 @@ public final class IncomingMessageHandler { - private final static Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class); + private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class); private final SignalAccount account; private final SignalDependencies dependencies; @@ -100,8 +101,10 @@ public Pair, Exception> handleRetryEnvelope( SignalServiceContent content = null; if (!envelope.isReceipt()) { account.getIdentityKeyStore().setRetryingDecryption(true); + final var destination = getDestination(envelope).serviceId(); try { - final var cipherResult = dependencies.getCipher() + final var cipherResult = dependencies.getCipher(destination == null + || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); if (content == null) { @@ -136,8 +139,10 @@ public Pair, Exception> handleEnvelope( // uuid in envelope is sent by server .ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId)); if (!envelope.isReceipt()) { + final var destination = getDestination(envelope).serviceId(); try { - final var cipherResult = dependencies.getCipher() + final var cipherResult = dependencies.getCipher(destination == null + || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); if (content == null) { @@ -173,7 +178,6 @@ public Pair, Exception> handleEnvelope( .contains(Profile.Capability.senderKey); final var isSelfSenderKeyCapable = selfProfile != null && selfProfile.getCapabilities() .contains(Profile.Capability.senderKey); - final var destination = getDestination(envelope).serviceId(); if (!isSelf && isSenderSenderKeyCapable && isSelfSenderKeyCapable) { logger.debug("Received invalid message, requesting message resend."); actions.add(new SendRetryMessageRequestAction(sender, serviceId, e, envelope, destination)); @@ -299,6 +303,12 @@ public List handleMessage( final var senderDeviceId = senderDeviceAddress.deviceId(); final var destination = getDestination(envelope); + if (account.getPni().equals(destination.serviceId)) { + account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true); + } else if (account.getAci().equals(destination.serviceId)) { + account.getRecipientStore().markNeedsPniSignature(destination.recipientId, false); + } + if (content.getReceiptMessage().isPresent()) { final var message = content.getReceiptMessage().get(); if (message.isDeliveryReceipt()) { @@ -511,6 +521,7 @@ private List handleSyncMessage( if (rm.isConfigurationRequest()) { actions.add(SendSyncConfigurationAction.create()); } + actions.add(SyncStorageDataAction.create()); } if (syncMessage.getGroups().isPresent()) { try { @@ -578,7 +589,7 @@ private List handleSyncMessage( if (syncMessage.getFetchType().isPresent()) { switch (syncMessage.getFetchType().get()) { case LOCAL_PROFILE -> actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); - case STORAGE_MANIFEST -> actions.add(RetrieveStorageDataAction.create()); + case STORAGE_MANIFEST -> actions.add(SyncStorageDataAction.create()); } } if (syncMessage.getKeys().isPresent()) { @@ -586,7 +597,12 @@ private List handleSyncMessage( if (keysMessage.getStorageService().isPresent()) { final var storageKey = keysMessage.getStorageService().get(); account.setStorageKey(storageKey); - actions.add(RetrieveStorageDataAction.create()); + actions.add(SyncStorageDataAction.create()); + } + if (keysMessage.getMaster().isPresent()) { + final var masterKey = keysMessage.getMaster().get(); + account.setMasterKey(masterKey); + actions.add(SyncStorageDataAction.create()); } } if (syncMessage.getConfiguration().isPresent()) { @@ -941,16 +957,12 @@ private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceCon } private DeviceAddress getDestination(SignalServiceEnvelope envelope) { - if (!envelope.hasDestinationUuid()) { - return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId()); - } - final var addressOptional = SignalServiceAddress.fromRaw(envelope.getDestinationServiceId(), null); - if (addressOptional.isEmpty()) { + final var destination = envelope.getDestinationServiceId(); + if (destination == null) { return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId()); } - final var address = addressOptional.get(); - return new DeviceAddress(account.getRecipientResolver().resolveRecipient(address), - address.getServiceId(), + return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination), + destination, account.getDeviceId()); } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PinHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PinHelper.java index 3b3a5c6f9bcc3..30fec709fe670 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PinHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PinHelper.java @@ -5,39 +5,49 @@ import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; -import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2; import org.whispersystems.signalservice.internal.push.AuthCredentials; import org.whispersystems.signalservice.internal.push.LockedException; import java.io.IOException; +import java.util.List; public class PinHelper { - private final static Logger logger = LoggerFactory.getLogger(PinHelper.class); + private static final Logger logger = LoggerFactory.getLogger(PinHelper.class); - private final SecureValueRecoveryV2 secureValueRecoveryV2; + private final List secureValueRecoveries; - public PinHelper(final SecureValueRecoveryV2 secureValueRecoveryV2) { - this.secureValueRecoveryV2 = secureValueRecoveryV2; + public PinHelper(final List secureValueRecoveries) { + this.secureValueRecoveries = secureValueRecoveries; } public void setRegistrationLockPin( String pin, MasterKey masterKey ) throws IOException { - final var backupResponse = secureValueRecoveryV2.setPin(pin, masterKey).execute(); - if (backupResponse instanceof SecureValueRecovery.BackupResponse.Success) { - } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.ServerRejected) { - logger.warn("Backup svr2 failed: ServerRejected"); - } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.EnclaveNotFound) { - logger.warn("Backup svr2 failed: EnclaveNotFound"); - } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.ExposeFailure) { - logger.warn("Backup svr2 failed: ExposeFailure"); - } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.ApplicationError error) { - throw new IOException(error.getException()); - } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.NetworkError error) { - throw error.getException(); - } else { - throw new AssertionError("Unexpected response"); + IOException exception = null; + for (final var secureValueRecovery : secureValueRecoveries) { + try { + final var backupResponse = secureValueRecovery.setPin(pin, masterKey).execute(); + if (backupResponse instanceof SecureValueRecovery.BackupResponse.Success success) { + } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.ServerRejected serverRejected) { + logger.warn("Backup svr2 failed: ServerRejected"); + } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.EnclaveNotFound enclaveNotFound) { + logger.warn("Backup svr2 failed: EnclaveNotFound"); + } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.ExposeFailure exposeFailure) { + logger.warn("Backup svr2 failed: ExposeFailure"); + } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.ApplicationError error) { + throw new IOException(error.getException()); + } else if (backupResponse instanceof SecureValueRecovery.BackupResponse.NetworkError error) { + throw error.getException(); + } else { + throw new AssertionError("Unexpected response"); + } + } catch (IOException e) { + exception = e; + } + } + if (exception != null) { + throw exception; } } @@ -46,27 +56,47 @@ public void migrateRegistrationLockPin(String pin, MasterKey masterKey) throws I } public void removeRegistrationLockPin() throws IOException { - final var deleteResponse = secureValueRecoveryV2.deleteData(); - if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.Success) { - } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.ServerRejected) { - logger.warn("Delete svr2 failed: ServerRejected"); - } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.EnclaveNotFound) { - logger.warn("Delete svr2 failed: EnclaveNotFound"); - } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.ApplicationError error) { - throw new IOException(error.getException()); - } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.NetworkError error) { - throw error.getException(); - } else { - throw new AssertionError("Unexpected response"); + IOException exception = null; + for (final var secureValueRecovery : secureValueRecoveries) { + try { + final var deleteResponse = secureValueRecovery.deleteData(); + if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.Success success) { + } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.ServerRejected serverRejected) { + logger.warn("Delete svr2 failed: ServerRejected"); + } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.EnclaveNotFound enclaveNotFound) { + logger.warn("Delete svr2 failed: EnclaveNotFound"); + } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.ApplicationError error) { + throw new IOException(error.getException()); + } else if (deleteResponse instanceof SecureValueRecovery.DeleteResponse.NetworkError error) { + throw error.getException(); + } else { + throw new AssertionError("Unexpected response"); + } + } catch (IOException e) { + exception = e; + } + } + if (exception != null) { + throw exception; } } public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData( - String pin, LockedException e + String pin, LockedException lockedException ) throws IOException, IncorrectPinException { - var svr2Credentials = e.getSvr2Credentials(); + var svr2Credentials = lockedException.getSvr2Credentials(); if (svr2Credentials != null) { - return getRegistrationLockData(secureValueRecoveryV2, svr2Credentials, pin); + IOException exception = null; + for (final var secureValueRecovery : secureValueRecoveries) { + try { + return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin); + } catch (IOException e) { + exception = e; + } + } + if (exception != null) { + throw exception; + } } return null; @@ -85,7 +115,7 @@ public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData( throw new IOException(error.getException()); } else if (restoreResponse instanceof SecureValueRecovery.RestoreResponse.NetworkError error) { throw error.getException(); - } else if (restoreResponse instanceof SecureValueRecovery.RestoreResponse.Missing) { + } else if (restoreResponse instanceof SecureValueRecovery.RestoreResponse.Missing missing) { logger.debug("No SVR data stored for the given credentials."); return null; } else { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PreKeyHelper.java index c08da92c7efc5..a2b4dddef21c5 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PreKeyHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -24,7 +24,7 @@ public class PreKeyHelper { - private final static Logger logger = LoggerFactory.getLogger(PreKeyHelper.class); + private static final Logger logger = LoggerFactory.getLogger(PreKeyHelper.class); private final SignalAccount account; private final SignalDependencies dependencies; @@ -41,6 +41,11 @@ public void refreshPreKeysIfNecessary() throws IOException { refreshPreKeysIfNecessary(ServiceIdType.PNI); } + public void forceRefreshPreKeys() throws IOException { + forceRefreshPreKeys(ServiceIdType.ACI); + forceRefreshPreKeys(ServiceIdType.PNI); + } + public void refreshPreKeysIfNecessary(ServiceIdType serviceIdType) throws IOException { final var identityKeyPair = account.getIdentityKeyPair(serviceIdType); if (identityKeyPair == null) { @@ -51,6 +56,30 @@ public void refreshPreKeysIfNecessary(ServiceIdType serviceIdType) throws IOExce return; } + if (refreshPreKeysIfNecessary(serviceIdType, identityKeyPair)) { + refreshPreKeysIfNecessary(serviceIdType, identityKeyPair); + } + } + + public void forceRefreshPreKeys(ServiceIdType serviceIdType) throws IOException { + final var identityKeyPair = account.getIdentityKeyPair(serviceIdType); + if (identityKeyPair == null) { + return; + } + final var accountId = account.getAccountId(serviceIdType); + if (accountId == null) { + return; + } + + final var counts = new OneTimePreKeyCounts(0, 0); + if (refreshPreKeysIfNecessary(serviceIdType, identityKeyPair, counts, true)) { + refreshPreKeysIfNecessary(serviceIdType, identityKeyPair, counts, true); + } + } + + private boolean refreshPreKeysIfNecessary( + final ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair + ) throws IOException { OneTimePreKeyCounts preKeyCounts; try { preKeyCounts = dependencies.getAccountManager().getPreKeyCounts(serviceIdType); @@ -59,68 +88,100 @@ public void refreshPreKeysIfNecessary(ServiceIdType serviceIdType) throws IOExce preKeyCounts = new OneTimePreKeyCounts(0, 0); } - SignedPreKeyRecord signedPreKeyRecord = null; - List preKeyRecords = null; - KyberPreKeyRecord lastResortKyberPreKeyRecord = null; - List kyberPreKeyRecords = null; + return refreshPreKeysIfNecessary(serviceIdType, identityKeyPair, preKeyCounts, false); + } - try { - if (preKeyCounts.getEcCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { - logger.debug("Refreshing {} ec pre keys, because only {} of min {} pre keys remain", - serviceIdType, - preKeyCounts.getEcCount(), - ServiceConfig.PREKEY_MINIMUM_COUNT); - preKeyRecords = generatePreKeys(serviceIdType); - } - if (signedPreKeyNeedsRefresh(serviceIdType)) { - logger.debug("Refreshing {} signed pre key.", serviceIdType); - signedPreKeyRecord = generateSignedPreKey(serviceIdType, identityKeyPair); - } - } catch (Exception e) { - logger.warn("Failed to store new pre keys, resetting preKey id offset", e); - account.resetPreKeyOffsets(serviceIdType); + private boolean refreshPreKeysIfNecessary( + final ServiceIdType serviceIdType, + final IdentityKeyPair identityKeyPair, + final OneTimePreKeyCounts preKeyCounts, + final boolean force + ) throws IOException { + List preKeyRecords = null; + if (force || preKeyCounts.getEcCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { + logger.debug("Refreshing {} ec pre keys, because only {} of min {} pre keys remain", + serviceIdType, + preKeyCounts.getEcCount(), + ServiceConfig.PREKEY_MINIMUM_COUNT); preKeyRecords = generatePreKeys(serviceIdType); + } + + SignedPreKeyRecord signedPreKeyRecord = null; + if (force || signedPreKeyNeedsRefresh(serviceIdType)) { + logger.debug("Refreshing {} signed pre key.", serviceIdType); signedPreKeyRecord = generateSignedPreKey(serviceIdType, identityKeyPair); } + List kyberPreKeyRecords = null; + if (force || preKeyCounts.getKyberCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { + logger.debug("Refreshing {} kyber pre keys, because only {} of min {} pre keys remain", + serviceIdType, + preKeyCounts.getKyberCount(), + ServiceConfig.PREKEY_MINIMUM_COUNT); + kyberPreKeyRecords = generateKyberPreKeys(serviceIdType, identityKeyPair); + } + + KyberPreKeyRecord lastResortKyberPreKeyRecord = null; + if (force || lastResortKyberPreKeyNeedsRefresh(serviceIdType)) { + logger.debug("Refreshing {} last resort kyber pre key.", serviceIdType); + lastResortKyberPreKeyRecord = generateLastResortKyberPreKey(serviceIdType, + identityKeyPair, + kyberPreKeyRecords == null ? 0 : kyberPreKeyRecords.size()); + } + + if (signedPreKeyRecord == null + && preKeyRecords == null + && lastResortKyberPreKeyRecord == null + && kyberPreKeyRecords == null) { + return false; + } + + final var preKeyUpload = new PreKeyUpload(serviceIdType, + signedPreKeyRecord, + preKeyRecords, + lastResortKyberPreKeyRecord, + kyberPreKeyRecords); + var needsReset = false; try { - if (preKeyCounts.getKyberCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { - logger.debug("Refreshing {} kyber pre keys, because only {} of min {} pre keys remain", - serviceIdType, - preKeyCounts.getKyberCount(), - ServiceConfig.PREKEY_MINIMUM_COUNT); - kyberPreKeyRecords = generateKyberPreKeys(serviceIdType, identityKeyPair); - } - if (lastResortKyberPreKeyNeedsRefresh(serviceIdType)) { - logger.debug("Refreshing {} last resort kyber pre key.", serviceIdType); - lastResortKyberPreKeyRecord = generateLastResortKyberPreKey(serviceIdType, identityKeyPair); + dependencies.getAccountManager().setPreKeys(preKeyUpload); + try { + if (preKeyRecords != null) { + account.addPreKeys(serviceIdType, preKeyRecords); + } + if (signedPreKeyRecord != null) { + account.addSignedPreKey(serviceIdType, signedPreKeyRecord); + } + } catch (Exception e) { + logger.warn("Failed to store new pre keys, resetting preKey id offset", e); + account.resetPreKeyOffsets(serviceIdType); + needsReset = true; } - } catch (Exception e) { - logger.warn("Failed to store new kyber pre keys, resetting preKey id offset", e); - account.resetKyberPreKeyOffsets(serviceIdType); - kyberPreKeyRecords = generateKyberPreKeys(serviceIdType, identityKeyPair); - lastResortKyberPreKeyRecord = generateLastResortKyberPreKey(serviceIdType, identityKeyPair); - } - - if (signedPreKeyRecord != null - || preKeyRecords != null - || lastResortKyberPreKeyRecord != null - || kyberPreKeyRecords != null) { - final var preKeyUpload = new PreKeyUpload(serviceIdType, - identityKeyPair.getPublicKey(), - signedPreKeyRecord, - preKeyRecords, - lastResortKyberPreKeyRecord, - kyberPreKeyRecords); try { - dependencies.getAccountManager().setPreKeys(preKeyUpload); - } catch (AuthorizationFailedException e) { - // This can happen when the primary device has changed phone number - logger.warn("Failed to updated pre keys: {}", e.getMessage()); + if (kyberPreKeyRecords != null) { + account.addKyberPreKeys(serviceIdType, kyberPreKeyRecords); + } + if (lastResortKyberPreKeyRecord != null) { + account.addLastResortKyberPreKey(serviceIdType, lastResortKyberPreKeyRecord); + } + } catch (Exception e) { + logger.warn("Failed to store new kyber pre keys, resetting preKey id offset", e); + account.resetKyberPreKeyOffsets(serviceIdType); + needsReset = true; } + } catch (AuthorizationFailedException e) { + // This can happen when the primary device has changed phone number + logger.warn("Failed to updated pre keys: {}", e.getMessage()); } + return needsReset; + } - cleanSignedPreKeys((serviceIdType)); + public void cleanOldPreKeys() { + cleanOldPreKeys(ServiceIdType.ACI); + cleanOldPreKeys(ServiceIdType.PNI); + } + + private void cleanOldPreKeys(final ServiceIdType serviceIdType) { + cleanSignedPreKeys(serviceIdType); cleanOneTimePreKeys(serviceIdType); } @@ -128,10 +189,7 @@ private List generatePreKeys(ServiceIdType serviceIdType) { final var accountData = account.getAccountData(serviceIdType); final var offset = accountData.getPreKeyMetadata().getNextPreKeyId(); - var records = KeyUtils.generatePreKeyRecords(offset); - account.addPreKeys(serviceIdType, records); - - return records; + return KeyUtils.generatePreKeyRecords(offset); } private boolean signedPreKeyNeedsRefresh(ServiceIdType serviceIdType) { @@ -153,10 +211,7 @@ private SignedPreKeyRecord generateSignedPreKey(ServiceIdType serviceIdType, Ide final var accountData = account.getAccountData(serviceIdType); final var signedPreKeyId = accountData.getPreKeyMetadata().getNextSignedPreKeyId(); - var record = KeyUtils.generateSignedPreKeyRecord(signedPreKeyId, identityKeyPair.getPrivateKey()); - account.addSignedPreKey(serviceIdType, record); - - return record; + return KeyUtils.generateSignedPreKeyRecord(signedPreKeyId, identityKeyPair.getPrivateKey()); } private List generateKyberPreKeys( @@ -165,10 +220,7 @@ private List generateKyberPreKeys( final var accountData = account.getAccountData(serviceIdType); final var offset = accountData.getPreKeyMetadata().getNextKyberPreKeyId(); - var records = KeyUtils.generateKyberPreKeyRecords(offset, identityKeyPair.getPrivateKey()); - account.addKyberPreKeys(serviceIdType, records); - - return records; + return KeyUtils.generateKyberPreKeyRecords(offset, identityKeyPair.getPrivateKey()); } private boolean lastResortKyberPreKeyNeedsRefresh(ServiceIdType serviceIdType) { @@ -188,15 +240,12 @@ private boolean lastResortKyberPreKeyNeedsRefresh(ServiceIdType serviceIdType) { } private KyberPreKeyRecord generateLastResortKyberPreKey( - ServiceIdType serviceIdType, IdentityKeyPair identityKeyPair + ServiceIdType serviceIdType, IdentityKeyPair identityKeyPair, final int offset ) { final var accountData = account.getAccountData(serviceIdType); - final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId(); - - var record = KeyUtils.generateKyberPreKeyRecord(signedPreKeyId, identityKeyPair.getPrivateKey()); - account.addLastResortKyberPreKey(serviceIdType, record); + final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId() + offset; - return record; + return KeyUtils.generateKyberPreKeyRecord(signedPreKeyId, identityKeyPair.getPrivateKey()); } private void cleanSignedPreKeys(ServiceIdType serviceIdType) { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ProfileHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ProfileHelper.java index 1631ad9d37323..5d6b90d453316 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -2,9 +2,11 @@ import org.asamk.signal.manager.api.GroupNotFoundException; import org.asamk.signal.manager.api.NotAGroupMemberException; +import org.asamk.signal.manager.api.PhoneNumberSharingMode; import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.internal.SignalDependencies; +import org.asamk.signal.manager.jobs.SyncStorageJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.recipients.RecipientAddress; @@ -47,7 +49,7 @@ public final class ProfileHelper { - private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class); + private static final Logger logger = LoggerFactory.getLogger(ProfileHelper.class); private final SignalAccount account; private final SignalDependencies dependencies; @@ -66,7 +68,8 @@ public void rotateProfileKey() throws IOException { account.setProfileKey(profileKey); context.getAccountHelper().updateAccountAttributes(); setProfile(true, true, null, null, null, null, null, null); - // TODO update profile key in storage + account.getRecipientStore().rotateSelfStorageId(); + context.getJobExecutor().enqueueJob(new SyncStorageJob()); final var recipientIds = account.getRecipientStore().getRecipientIdsWithEnabledProfileSharing(); for (final var recipientId : recipientIds) { @@ -77,7 +80,7 @@ public void rotateProfileKey() throws IOException { final var activeGroupIds = account.getGroupStore() .getGroups() .stream() - .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId)) + .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId) && g.isProfileSharingEnabled()) .map(g -> (GroupInfoV2) g) .map(GroupInfoV2::getGroupId) .toList(); @@ -202,7 +205,9 @@ public void setProfile( newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), paymentsAddress, avatarUploadParams, - List.of(/* TODO implement support for badges */)); + List.of(/* TODO implement support for badges */), + account.getConfigurationStore().getPhoneNumberSharingMode() + == PhoneNumberSharingMode.EVERYBODY); if (!avatarUploadParams.keepTheSame) { builder.withAvatarUrlPath(avatarPath.orElse(null)); } @@ -325,6 +330,13 @@ private Single retrieveProfile( final var profile = account.getProfileStore().getProfile(recipientId); + if (recipientId.equals(account.getSelfRecipientId())) { + final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess(); + if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) { + account.setUnrestrictedUnidentifiedAccess(isUnrestricted); + } + } + Profile newProfile = null; if (profileKey.isPresent()) { logger.trace("Decrypting profile"); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ReceiveHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ReceiveHelper.java index d1547a0078976..a66d6ff621850 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ReceiveHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/ReceiveHelper.java @@ -5,6 +5,7 @@ import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.manager.api.UntrustedIdentityException; import org.asamk.signal.manager.internal.SignalDependencies; +import org.asamk.signal.manager.jobs.CleanOldPreKeysJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.RecipientAddress; @@ -32,8 +33,8 @@ public class ReceiveHelper { - private final static Logger logger = LoggerFactory.getLogger(ReceiveHelper.class); - private final static int MAX_BACKOFF_COUNTER = 9; + private static final Logger logger = LoggerFactory.getLogger(ReceiveHelper.class); + private static final int MAX_BACKOFF_COUNTER = 9; private final SignalAccount account; private final SignalDependencies dependencies; @@ -176,6 +177,7 @@ private void receiveMessagesInternal( handleQueuedActions(queuedActions.keySet()); queuedActions.clear(); + context.getJobExecutor().enqueueJob(new CleanOldPreKeysJob()); hasCaughtUpWithOldMessages = true; caughtUpWithOldMessagesListener.call(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/RecipientHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/RecipientHelper.java index 029ccc499ed9a..947c251b0ae57 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -2,6 +2,7 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.UnregisteredRecipientException; +import org.asamk.signal.manager.api.UsernameLinkUrl; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.storage.SignalAccount; @@ -29,7 +30,7 @@ public class RecipientHelper { - private final static Logger logger = LoggerFactory.getLogger(RecipientHelper.class); + private static final Logger logger = LoggerFactory.getLogger(RecipientHelper.class); private final SignalAccount account; private final SignalDependencies dependencies; @@ -89,10 +90,23 @@ public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) } }); } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) { - final var username = usernameRecipient.username(); - return account.getRecipientStore().resolveRecipientByUsername(username, () -> { + var username = usernameRecipient.username(); + try { + UsernameLinkUrl usernameLinkUrl = UsernameLinkUrl.fromUri(username); + final var components = usernameLinkUrl.getComponents(); + final var encryptedUsername = dependencies.getAccountManager() + .getEncryptedUsernameFromLinkServerId(components.getServerId()); + final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername); + + username = Username.fromLink(link).getUsername(); + } catch (UsernameLinkUrl.InvalidUsernameLinkException e) { + } catch (IOException | BaseUsernameException e) { + throw new RuntimeException(e); + } + final String finalUsername = username; + return account.getRecipientStore().resolveRecipientByUsername(finalUsername, () -> { try { - return getRegisteredUserByUsername(username); + return getRegisteredUserByUsername(finalUsername); } catch (Exception e) { return null; } @@ -144,12 +158,16 @@ public Map getRegisteredUsers( private Map getRegisteredUsers( final Set numbers, final boolean isPartialRefresh ) throws IOException { - Map registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh, true); + Map registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh); // Store numbers as recipients, so we have the number/uuid association registeredUsers.forEach((number, u) -> account.getRecipientTrustedResolver() .resolveRecipientTrusted(u.aci, u.pni, Optional.of(number))); + final var unregisteredUsers = new HashSet<>(numbers); + unregisteredUsers.removeAll(registeredUsers.keySet()); + account.getRecipientStore().markUnregistered(unregisteredUsers); + return registeredUsers; } @@ -168,7 +186,7 @@ private ServiceId getRegisteredUserByNumber(final String number) throws IOExcept } private Map getRegisteredUsersV2( - final Set numbers, boolean isPartialRefresh, boolean useCompat + final Set numbers, boolean isPartialRefresh ) throws IOException { final var previousNumbers = isPartialRefresh ? Set.of() : account.getCdsiStore().getAllNumbers(); final var newNumbers = new HashSet<>(numbers) {{ @@ -192,7 +210,6 @@ private Map getRegisteredUsersV2( .getRegisteredUsersWithCdsi(previousNumbers, newNumbers, account.getRecipientStore().getServiceIdToProfileKeyMap(), - useCompat, token, serviceEnvironmentConfig.cdsiMrenclave(), null, diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SendHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SendHelper.java index 0c5dc70ec6083..bd6bb3ebf3fc9 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SendHelper.java @@ -56,7 +56,7 @@ public class SendHelper { - private final static Logger logger = LoggerFactory.getLogger(SendHelper.class); + private static final Logger logger = LoggerFactory.getLogger(SendHelper.class); private final SignalAccount account; private final SignalDependencies dependencies; @@ -78,13 +78,13 @@ public SendMessageResult sendMessage( Optional editTargetTimestamp ) { var contact = account.getContactStore().getContact(recipientId); - if (contact == null || !contact.isProfileSharingEnabled()) { + if (contact == null || !contact.isProfileSharingEnabled() || contact.isHidden()) { final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - contact = contactBuilder.withProfileSharingEnabled(true).build(); + contact = contactBuilder.withIsProfileSharingEnabled(true).withIsHidden(false).build(); account.getContactStore().storeContact(recipientId, contact); } - final var expirationTime = contact.getMessageExpirationTime(); + final var expirationTime = contact.messageExpirationTime(); messageBuilder.withExpiration(expirationTime); if (!contact.isBlocked()) { @@ -101,10 +101,13 @@ public SendMessageResult sendMessage( * The message is extended with the current expiration timer for the group and the group context. */ public List sendAsGroupMessage( - SignalServiceDataMessage.Builder messageBuilder, GroupId groupId, Optional editTargetTimestamp + final SignalServiceDataMessage.Builder messageBuilder, + final GroupId groupId, + final boolean includeSelf, + final Optional editTargetTimestamp ) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException { final var g = getGroupForSending(groupId); - return sendAsGroupMessage(messageBuilder, g, editTargetTimestamp); + return sendAsGroupMessage(messageBuilder, g, includeSelf, editTargetTimestamp); } /** @@ -124,10 +127,10 @@ public SendMessageResult sendReceiptMessage( ) { final var messageSendLogStore = account.getMessageSendLogStore(); final var result = handleSendMessage(recipientId, - (messageSender, address, unidentifiedAccess) -> messageSender.sendReceipt(address, + (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendReceipt(address, unidentifiedAccess, receiptMessage, - false)); + includePniSignature)); messageSendLogStore.insertIfPossible(receiptMessage.getWhen(), result, ContentHint.IMPLICIT, false); handleSendMessageResult(result); return result; @@ -141,13 +144,14 @@ public SendMessageResult sendProfileKey(RecipientId recipientId) { .withProfileKey(profileKey) .build(); return handleSendMessage(recipientId, - (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address, + (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage( + address, unidentifiedAccess, ContentHint.IMPLICIT, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, false, - false)); + includePniSignature)); } public SendMessageResult sendRetryReceipt( @@ -158,7 +162,8 @@ public SendMessageResult sendRetryReceipt( recipientId, errorMessage.getDeviceId()); final var result = handleSendMessage(recipientId, - (messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address, + (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendRetryReceipt( + address, unidentifiedAccess, groupId.map(GroupId::serialize), errorMessage)); @@ -167,7 +172,10 @@ public SendMessageResult sendRetryReceipt( } public SendMessageResult sendNullMessage(RecipientId recipientId) { - final var result = handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage); + final var result = handleSendMessage(recipientId, + (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendNullMessage( + address, + unidentifiedAccess)); handleSendMessageResult(result); return result; } @@ -177,7 +185,7 @@ public SendMessageResult sendSelfMessage( ) { final var recipientId = account.getSelfRecipientId(); final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + final var expirationTime = contact != null ? contact.messageExpirationTime() : 0; messageBuilder.withExpiration(expirationTime); var message = messageBuilder.build(); @@ -217,10 +225,8 @@ public SendMessageResult sendTypingMessage( SignalServiceTypingMessage message, RecipientId recipientId ) { final var result = handleSendMessage(recipientId, - (messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(List.of(address), - List.of(unidentifiedAccess), - message, - null).get(0)); + (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of( + address), List.of(unidentifiedAccess), message, null).get(0)); handleSendMessageResult(result); return result; } @@ -244,7 +250,8 @@ public SendMessageResult resendMessage( logger.trace("Resending message {} to {}", timestamp, recipientId); if (messageSendLogEntry.groupId().isEmpty()) { return handleSendMessage(recipientId, - (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address, + (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.resendContent( + address, unidentifiedAccess, timestamp, messageSendLogEntry.content(), @@ -274,7 +281,7 @@ public SendMessageResult resendMessage( .build(); final var result = handleSendMessage(recipientId, - (messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address, + (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.resendContent(address, unidentifiedAccess, timestamp, contentToSend, @@ -297,13 +304,16 @@ public SendMessageResult resendMessage( } private List sendAsGroupMessage( - final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g, Optional editTargetTimestamp + final SignalServiceDataMessage.Builder messageBuilder, + final GroupInfo g, + final boolean includeSelf, + final Optional editTargetTimestamp ) throws IOException, GroupSendingNotAllowedException { GroupUtils.setGroupContext(messageBuilder, g); messageBuilder.withExpiration(g.getMessageExpirationTimer()); final var message = messageBuilder.build(); - final var recipients = g.getMembersWithout(account.getSelfRecipientId()); + final var recipients = includeSelf ? g.getMembers() : g.getMembersWithout(account.getSelfRecipientId()); if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { if (message.getBody().isPresent() @@ -448,6 +458,10 @@ private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundExcept if (!g.isMember(account.getSelfRecipientId())) { throw new NotAGroupMemberException(groupId, g.getTitle()); } + if (!g.isProfileSharingEnabled()) { + g.setProfileSharingEnabled(true); + account.getGroupStore().updateGroup(g); + } return g; } @@ -650,17 +664,18 @@ private SendMessageResult sendMessage( ) { final var messageSendLogStore = account.getMessageSendLogStore(); final var urgent = true; - final var includePniSignature = false; final var result = handleSendMessage(recipientId, editTargetTimestamp.isEmpty() - ? (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address, + ? (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage( + address, unidentifiedAccess, ContentHint.RESENDABLE, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, includePniSignature) - : (messageSender, address, unidentifiedAccess) -> messageSender.sendEditMessage(address, + : (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendEditMessage( + address, unidentifiedAccess, ContentHint.RESENDABLE, message, @@ -677,8 +692,12 @@ private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandl var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId); try { + final boolean includePniSignature = account.getRecipientStore().needsPniSignature(recipientId); try { - return s.send(messageSender, address, context.getUnidentifiedAccessHelper().getAccessFor(recipientId)); + return s.send(messageSender, + address, + context.getUnidentifiedAccessHelper().getAccessFor(recipientId), + includePniSignature); } catch (UnregisteredUserException e) { final RecipientId newRecipientId; try { @@ -689,7 +708,8 @@ private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandl address = context.getRecipientHelper().resolveSignalServiceAddress(newRecipientId); return s.send(messageSender, address, - context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId)); + context.getUnidentifiedAccessHelper().getAccessFor(newRecipientId), + includePniSignature); } } catch (UnregisteredUserException e) { return SendMessageResult.unregisteredFailure(address); @@ -764,7 +784,8 @@ interface SenderHandler { SendMessageResult send( SignalServiceMessageSender messageSender, SignalServiceAddress address, - Optional unidentifiedAccess + Optional unidentifiedAccess, + boolean includePniSignature ) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StickerHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StickerHelper.java index daa5aeb608204..4a0d25feb3be4 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StickerHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StickerHelper.java @@ -17,7 +17,7 @@ public class StickerHelper { - private final static Logger logger = LoggerFactory.getLogger(StickerHelper.class); + private static final Logger logger = LoggerFactory.getLogger(StickerHelper.class); private final Context context; private final SignalAccount account; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StorageHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StorageHelper.java index e9a8d68163aa8..a0c693e597cc8 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -1,37 +1,48 @@ package org.asamk.signal.manager.helper; -import org.asamk.signal.manager.api.Contact; -import org.asamk.signal.manager.api.GroupId; -import org.asamk.signal.manager.api.PhoneNumberSharingMode; -import org.asamk.signal.manager.api.Profile; -import org.asamk.signal.manager.api.TrustLevel; +import org.asamk.signal.manager.api.GroupIdV1; +import org.asamk.signal.manager.api.GroupIdV2; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.storage.SignalAccount; -import org.asamk.signal.manager.storage.recipients.RecipientAddress; -import org.signal.libsignal.protocol.IdentityKey; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.syncStorage.AccountRecordProcessor; +import org.asamk.signal.manager.syncStorage.ContactRecordProcessor; +import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor; +import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor; +import org.asamk.signal.manager.syncStorage.StorageSyncModels; +import org.asamk.signal.manager.syncStorage.StorageSyncValidations; +import org.asamk.signal.manager.syncStorage.WriteOperationResult; +import org.asamk.signal.manager.util.KeyUtils; +import org.signal.core.util.SetUtil; import org.signal.libsignal.protocol.InvalidKeyException; -import org.signal.libsignal.zkgroup.InvalidInputException; -import org.signal.libsignal.zkgroup.groups.GroupMasterKey; -import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord.Identifier.Type; import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class StorageHelper { - private final static Logger logger = LoggerFactory.getLogger(StorageHelper.class); + private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class); + private static final List KNOWN_TYPES = List.of(ManifestRecord.Identifier.Type.CONTACT.getValue(), + ManifestRecord.Identifier.Type.GROUPV1.getValue(), + ManifestRecord.Identifier.Type.GROUPV2.getValue(), + ManifestRecord.Identifier.Type.ACCOUNT.getValue()); private final SignalAccount account; private final SignalDependencies dependencies; @@ -43,272 +54,514 @@ public StorageHelper(final Context context) { this.context = context; } - public void readDataFromStorage() throws IOException { + public void syncDataWithStorage() throws IOException { final var storageKey = account.getOrCreateStorageKey(); if (storageKey == null) { - logger.debug("Storage key unknown, requesting from primary device."); - context.getSyncHelper().requestSyncKeys(); + if (!account.isPrimaryDevice()) { + logger.debug("Storage key unknown, requesting from primary device."); + context.getSyncHelper().requestSyncKeys(); + } return; } - logger.debug("Reading data from remote storage"); - Optional manifest; + logger.trace("Reading manifest from remote storage"); + final var localManifestVersion = account.getStorageManifestVersion(); + final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY); + SignalStorageManifest remoteManifest; try { - manifest = dependencies.getAccountManager() - .getStorageManifestIfDifferentVersion(storageKey, account.getStorageManifestVersion()); + remoteManifest = dependencies.getAccountManager() + .getStorageManifestIfDifferentVersion(storageKey, localManifestVersion) + .orElse(localManifest); } catch (InvalidKeyException e) { - logger.warn("Manifest couldn't be decrypted, ignoring."); + logger.warn("Manifest couldn't be decrypted."); + if (account.isPrimaryDevice()) { + try { + forcePushToStorage(storageKey); + } catch (RetryLaterException rle) { + // TODO retry later + return; + } + } return; } - if (manifest.isEmpty()) { - logger.debug("Manifest is up to date, does not exist or couldn't be decrypted, ignoring."); - return; + logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.getVersion()); + + var needsForcePush = false; + if (remoteManifest.getVersion() > localManifestVersion) { + logger.trace("Remote version was newer, reading records."); + needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest); + } else if (remoteManifest.getVersion() < localManifest.getVersion()) { + logger.debug("Remote storage manifest version was older. User might have switched accounts."); } + logger.trace("Done reading data from remote storage"); - logger.trace("Remote storage manifest has {} records", manifest.get().getStorageIds().size()); - final var storageIds = manifest.get() - .getStorageIds() - .stream() - .filter(id -> !id.isUnknown()) - .collect(Collectors.toSet()); + if (localManifest != remoteManifest) { + storeManifestLocally(remoteManifest); + } - Optional localManifest = account.getStorageManifest(); - localManifest.ifPresent(m -> m.getStorageIds().forEach(storageIds::remove)); + readRecordsWithPreviouslyUnknownTypes(storageKey); - logger.trace("Reading {} new records", manifest.get().getStorageIds().size()); - for (final var record : getSignalStorageRecords(storageIds)) { - logger.debug("Reading record of type {}", record.getType()); - if (record.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) { - readAccountRecord(record); - } else if (record.getType() == ManifestRecord.Identifier.Type.GROUPV2.getValue()) { - readGroupV2Record(record); - } else if (record.getType() == ManifestRecord.Identifier.Type.GROUPV1.getValue()) { - readGroupV1Record(record); - } else if (record.getType() == ManifestRecord.Identifier.Type.CONTACT.getValue()) { - readContactRecord(record); - } - } - account.setStorageManifestVersion(manifest.get().getVersion()); - account.setStorageManifest(manifest.get()); - logger.debug("Done reading data from remote storage"); - } + logger.trace("Adding missing storageIds to local data"); + account.getRecipientStore().setMissingStorageIds(); + account.getGroupStore().setMissingStorageIds(); - private void readContactRecord(final SignalStorageRecord record) { - if (record == null || record.getContact().isEmpty()) { + var needsMultiDeviceSync = false; + try { + needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush); + } catch (RetryLaterException e) { + // TODO retry later return; } - final var contactRecord = record.getContact().get(); - final var aci = contactRecord.getAci().orElse(null); - final var pni = contactRecord.getPni().orElse(null); - if (contactRecord.getNumber().isEmpty() && aci == null && pni == null) { - return; + if (needsForcePush) { + logger.debug("Doing a force push."); + try { + forcePushToStorage(storageKey); + needsMultiDeviceSync = true; + } catch (RetryLaterException e) { + // TODO retry later + return; + } } - final var address = new RecipientAddress(aci, pni, contactRecord.getNumber().orElse(null)); - var recipientId = account.getRecipientResolver().resolveRecipient(address); - if (aci != null && contactRecord.getUsername().isPresent()) { - recipientId = account.getRecipientTrustedResolver() - .resolveRecipientTrusted(aci, contactRecord.getUsername().get()); + + if (needsMultiDeviceSync) { + context.getSyncHelper().sendSyncFetchStorageMessage(); } - final var contact = account.getContactStore().getContact(recipientId); - final var blocked = contact != null && contact.isBlocked(); - final var profileShared = contact != null && contact.isProfileSharingEnabled(); - final var archived = contact != null && contact.isArchived(); - final var contactGivenName = contact == null ? null : contact.getGivenName(); - final var contactFamilyName = contact == null ? null : contact.getFamilyName(); - if (blocked != contactRecord.isBlocked() - || profileShared != contactRecord.isProfileSharingEnabled() - || archived != contactRecord.isArchived() - || ( - contactRecord.getSystemGivenName().isPresent() && !contactRecord.getSystemGivenName() - .get() - .equals(contactGivenName) - ) - || ( - contactRecord.getSystemFamilyName().isPresent() && !contactRecord.getSystemFamilyName() - .get() - .equals(contactFamilyName) - )) { - logger.debug("Storing new or updated contact {}", recipientId); - final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - final var newContact = contactBuilder.withBlocked(contactRecord.isBlocked()) - .withProfileSharingEnabled(contactRecord.isProfileSharingEnabled()) - .withArchived(contactRecord.isArchived()); - if (contactRecord.getSystemGivenName().isPresent() || contactRecord.getSystemFamilyName().isPresent()) { - newContact.withGivenName(contactRecord.getSystemGivenName().orElse(null)) - .withFamilyName(contactRecord.getSystemFamilyName().orElse(null)); + logger.debug("Done syncing data with remote storage"); + } + + private boolean readDataFromStorage( + final StorageKey storageKey, + final SignalStorageManifest localManifest, + final SignalStorageManifest remoteManifest + ) throws IOException { + var needsForcePush = false; + try (final var connection = account.getAccountDatabase().getConnection()) { + connection.setAutoCommit(false); + + var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds()); + + if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) { + logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes."); + needsForcePush = true; } - account.getContactStore().storeContact(recipientId, newContact.build()); - } - final var profile = account.getProfileStore().getProfile(recipientId); - final var profileGivenName = profile == null ? null : profile.getGivenName(); - final var profileFamilyName = profile == null ? null : profile.getFamilyName(); - if (( - contactRecord.getProfileGivenName().isPresent() && !contactRecord.getProfileGivenName() - .get() - .equals(profileGivenName) - ) || ( - contactRecord.getProfileFamilyName().isPresent() && !contactRecord.getProfileFamilyName() - .get() - .equals(profileFamilyName) - )) { - final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); - final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null)) - .withFamilyName(contactRecord.getProfileFamilyName().orElse(null)) - .build(); - account.getProfileStore().storeProfile(recipientId, newProfile); - } - if (contactRecord.getProfileKey().isPresent()) { - try { - logger.trace("Storing profile key {}", recipientId); - final var profileKey = new ProfileKey(contactRecord.getProfileKey().get()); - account.getProfileStore().storeProfileKey(recipientId, profileKey); - } catch (InvalidInputException e) { - logger.warn("Received invalid contact profile key from storage"); + logger.debug("Pre-Merge ID Difference :: " + idDifference); + + if (!idDifference.localOnlyIds().isEmpty()) { + final var updated = account.getRecipientStore() + .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, idDifference.localOnlyIds()); + + if (updated > 0) { + logger.warn( + "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", + updated); + } } - } - if (contactRecord.getIdentityKey().isPresent() && aci != null) { - try { - logger.trace("Storing identity key {}", recipientId); - final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get()); - account.getIdentityKeyStore().saveIdentity(aci, identityKey); - final var trustLevel = TrustLevel.fromIdentityState(contactRecord.getIdentityState()); - if (trustLevel != null) { - account.getIdentityKeyStore().setIdentityTrustLevel(aci, identityKey, trustLevel); + if (!idDifference.isEmpty()) { + final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds()); + + if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) { + logger.debug("Could not find all remote-only records! Requested: " + + idDifference.remoteOnlyIds() + .size() + + ", Found: " + + remoteOnlyRecords.size() + + ". These stragglers should naturally get deleted during the sync."); } - } catch (InvalidKeyException e) { - logger.warn("Received invalid contact identity key from storage"); + + final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); + final var unknownDeletes = idDifference.localOnlyIds() + .stream() + .filter(id -> !KNOWN_TYPES.contains(id.getType())) + .toList(); + + logger.debug("Storage ids with unknown type: {} inserts, {} deletes", + unknownInserts.size(), + unknownDeletes.size()); + + account.getUnknownStorageIdStore().addUnknownStorageIds(connection, unknownInserts); + account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, unknownDeletes); + } else { + logger.debug("Remote version was newer, but there were no remote-only IDs."); } + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed to sync remote storage", e); } + return needsForcePush; } - private void readGroupV1Record(final SignalStorageRecord record) { - if (record == null || record.getGroupV1().isEmpty()) { - return; - } + private void readRecordsWithPreviouslyUnknownTypes(final StorageKey storageKey) throws IOException { + try (final var connection = account.getAccountDatabase().getConnection()) { + connection.setAutoCommit(false); + final var knownUnknownIds = account.getUnknownStorageIdStore() + .getUnknownStorageIds(connection, KNOWN_TYPES); - final var groupV1Record = record.getGroupV1().get(); - final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId()); + if (!knownUnknownIds.isEmpty()) { + logger.debug("We have " + knownUnknownIds.size() + " unknown records that we can now process."); - var group = account.getGroupStore().getGroup(groupIdV1); - if (group == null) { - try { - context.getGroupHelper().sendGroupInfoRequest(groupIdV1, account.getSelfRecipientId()); - } catch (Throwable e) { - logger.warn("Failed to send group request", e); + final var remote = getSignalStorageRecords(storageKey, knownUnknownIds); + + logger.debug("Found " + remote.size() + " of the known-unknowns remotely."); + + processKnownRecords(connection, remote); + account.getUnknownStorageIdStore() + .deleteUnknownStorageIds(connection, remote.stream().map(SignalStorageRecord::getId).toList()); } - group = account.getGroupStore().getOrCreateGroupV1(groupIdV1); - } - if (group != null && group.isBlocked() != groupV1Record.isBlocked()) { - group.setBlocked(groupV1Record.isBlocked()); - account.getGroupStore().updateGroup(group); + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed to sync remote storage", e); } } - private void readGroupV2Record(final SignalStorageRecord record) { - if (record == null || record.getGroupV2().isEmpty()) { - return; + private boolean writeToStorage( + final StorageKey storageKey, final SignalStorageManifest remoteManifest, final boolean needsForcePush + ) throws IOException, RetryLaterException { + final WriteOperationResult remoteWriteOperation; + try (final var connection = account.getAccountDatabase().getConnection()) { + connection.setAutoCommit(false); + + final var localStorageIds = getAllLocalStorageIds(connection); + final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds); + logger.debug("ID Difference :: " + idDifference); + + final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList(); + final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds()); + // TODO check if local storage record proto matches remote, then reset to remote storage_id + + remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1, + account.getDeviceId(), + localStorageIds), remoteInserts, remoteDeletes); + + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed to sync remote storage", e); } - final var groupV2Record = record.getGroupV2().get(); - if (groupV2Record.isArchived()) { - return; + if (remoteWriteOperation.isEmpty()) { + logger.debug("No remote writes needed. Still at version: " + remoteManifest.getVersion()); + return false; } - final GroupMasterKey groupMasterKey; + logger.debug("We have something to write remotely."); + logger.debug("WriteOperationResult :: " + remoteWriteOperation); + + StorageSyncValidations.validate(remoteWriteOperation, + remoteManifest, + needsForcePush, + account.getSelfRecipientAddress()); + + final Optional conflict; try { - groupMasterKey = new GroupMasterKey(groupV2Record.getMasterKeyBytes()); - } catch (InvalidInputException e) { - logger.warn("Received invalid group master key from storage"); - return; + conflict = dependencies.getAccountManager() + .writeStorageRecords(storageKey, + remoteWriteOperation.manifest(), + remoteWriteOperation.inserts(), + remoteWriteOperation.deletes()); + } catch (InvalidKeyException e) { + logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage()); + throw new IOException(e); } - final var group = context.getGroupHelper().getOrMigrateGroup(groupMasterKey, 0, null); - if (group.isBlocked() != groupV2Record.isBlocked()) { - group.setBlocked(groupV2Record.isBlocked()); - account.getGroupStore().updateGroup(group); + if (conflict.isPresent()) { + logger.debug("Hit a conflict when trying to resolve the conflict! Retrying."); + throw new RetryLaterException(); } - } - private void readAccountRecord(final SignalStorageRecord record) throws IOException { - if (record == null) { - logger.warn("Could not find account record, even though we had an ID, ignoring."); - return; - } + logger.debug("Saved new manifest. Now at version: " + remoteWriteOperation.manifest().getVersion()); + storeManifestLocally(remoteWriteOperation.manifest()); - SignalAccountRecord accountRecord = record.getAccount().orElse(null); - if (accountRecord == null) { - logger.warn("The storage record didn't actually have an account, ignoring."); - return; - } + return true; + } - if (!accountRecord.getE164().equals(account.getNumber())) { - context.getAccountHelper().checkWhoAmiI(); - } + private void forcePushToStorage( + final StorageKey storageServiceKey + ) throws IOException, RetryLaterException { + logger.debug("Force pushing local state to remote storage"); + + final var currentVersion = dependencies.getAccountManager().getStorageManifestVersion(); + final var newVersion = currentVersion + 1; + final var newStorageRecords = new ArrayList(); + final Map newContactStorageIds; + final Map newGroupV1StorageIds; + final Map newGroupV2StorageIds; + + try (final var connection = account.getAccountDatabase().getConnection()) { + connection.setAutoCommit(false); + + final var recipientIds = account.getRecipientStore().getRecipientIds(connection); + newContactStorageIds = generateContactStorageIds(recipientIds); + for (final var recipientId : recipientIds) { + final var storageId = newContactStorageIds.get(recipientId); + if (storageId.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) { + final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); + final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), + recipient, + account.getUsernameLink(), + storageId.getRaw()); + newStorageRecords.add(accountRecord); + } else { + final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); + final var address = recipient.getAddress().getIdentifier(); + final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); + final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); + newStorageRecords.add(record); + } + } - account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled()); - account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled()); - account.getConfigurationStore() - .setUnidentifiedDeliveryIndicators(accountRecord.isSealedSenderIndicatorsEnabled()); - account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled()); - account.getConfigurationStore().setPhoneNumberSharingMode(switch (accountRecord.getPhoneNumberSharingMode()) { - case EVERYBODY -> PhoneNumberSharingMode.EVERYBODY; - case NOBODY, UNKNOWN -> PhoneNumberSharingMode.NOBODY; - }); - account.getConfigurationStore().setPhoneNumberUnlisted(accountRecord.isPhoneNumberUnlisted()); - account.setUsername(accountRecord.getUsername()); - - if (accountRecord.getProfileKey().isPresent()) { - ProfileKey profileKey; - try { - profileKey = new ProfileKey(accountRecord.getProfileKey().get()); - } catch (InvalidInputException e) { - logger.warn("Received invalid profile key from storage"); - profileKey = null; + final var groupV1Ids = account.getGroupStore().getGroupV1Ids(connection); + newGroupV1StorageIds = generateGroupV1StorageIds(groupV1Ids); + for (final var groupId : groupV1Ids) { + final var storageId = newGroupV1StorageIds.get(groupId); + final var group = account.getGroupStore().getGroup(connection, groupId); + final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); + newStorageRecords.add(record); } - if (profileKey != null) { - account.setProfileKey(profileKey); - final var avatarPath = accountRecord.getAvatarUrlPath().orElse(null); - context.getProfileHelper().downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, profileKey); + + final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection); + newGroupV2StorageIds = generateGroupV2StorageIds(groupV2Ids); + for (final var groupId : groupV2Ids) { + final var storageId = newGroupV2StorageIds.get(groupId); + final var group = account.getGroupStore().getGroup(connection, groupId); + final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); + newStorageRecords.add(record); } + + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed to sync remote storage", e); } + final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList(); - context.getProfileHelper() - .setProfile(false, - false, - accountRecord.getGivenName().orElse(null), - accountRecord.getFamilyName().orElse(null), - null, - null, - null, - null); - } + final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), newStorageIds); - private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException { - List records; + StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress()); + + final Optional conflict; try { - records = dependencies.getAccountManager() - .readStorageRecords(account.getStorageKey(), Collections.singletonList(accountId)); + if (newVersion > 1) { + logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size()); + conflict = dependencies.getAccountManager() + .resetStorageRecords(storageServiceKey, manifest, newStorageRecords); + } else { + logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size()); + conflict = dependencies.getAccountManager() + .writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList()); + } } catch (InvalidKeyException e) { - logger.warn("Failed to read storage records, ignoring."); - return null; + logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e); + throw new RetryLaterException(); } - return !records.isEmpty() ? records.get(0) : null; + + if (conflict.isPresent()) { + logger.debug("Hit a conflict. Trying again."); + throw new RetryLaterException(); + } + + logger.debug("Force push succeeded. Updating local manifest version to: " + manifest.getVersion()); + storeManifestLocally(manifest); + + try (final var connection = account.getAccountDatabase().getConnection()) { + connection.setAutoCommit(false); + account.getRecipientStore().updateStorageIds(connection, newContactStorageIds); + account.getGroupStore().updateStorageIds(connection, newGroupV1StorageIds, newGroupV2StorageIds); + + // delete all unknown storage ids + account.getUnknownStorageIdStore().deleteAllUnknownStorageIds(connection); + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed to sync remote storage", e); + } + } + + private Map generateContactStorageIds(List recipientIds) { + final var selfRecipientId = account.getSelfRecipientId(); + return recipientIds.stream().collect(Collectors.toMap(recipientId -> recipientId, recipientId -> { + if (recipientId.equals(selfRecipientId)) { + return StorageId.forAccount(KeyUtils.createRawStorageId()); + } else { + return StorageId.forContact(KeyUtils.createRawStorageId()); + } + })); + } + + private Map generateGroupV1StorageIds(List groupIds) { + return groupIds.stream() + .collect(Collectors.toMap(recipientId -> recipientId, + recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId()))); + } + + private Map generateGroupV2StorageIds(List groupIds) { + return groupIds.stream() + .collect(Collectors.toMap(recipientId -> recipientId, + recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId()))); + } + + private void storeManifestLocally( + final SignalStorageManifest remoteManifest + ) { + account.setStorageManifestVersion(remoteManifest.getVersion()); + account.setStorageManifest(remoteManifest); } - private List getSignalStorageRecords(final Collection storageIds) throws IOException { + private List getSignalStorageRecords( + final StorageKey storageKey, final List storageIds + ) throws IOException { List records; try { - records = dependencies.getAccountManager() - .readStorageRecords(account.getStorageKey(), new ArrayList<>(storageIds)); + records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds); } catch (InvalidKeyException e) { logger.warn("Failed to read storage records, ignoring."); return List.of(); } return records; } + + private List getAllLocalStorageIds(final Connection connection) throws SQLException { + final var storageIds = new ArrayList(); + storageIds.addAll(account.getUnknownStorageIdStore().getUnknownStorageIds(connection)); + storageIds.addAll(account.getGroupStore().getStorageIds(connection)); + storageIds.addAll(account.getRecipientStore().getStorageIds(connection)); + storageIds.add(account.getRecipientStore().getSelfStorageId(connection)); + return storageIds; + } + + private List buildLocalStorageRecords( + final Connection connection, final List storageIds + ) throws SQLException { + final var records = new ArrayList(); + for (final var storageId : storageIds) { + final var record = buildLocalStorageRecord(connection, storageId); + if (record != null) { + records.add(record); + } + } + return records; + } + + private SignalStorageRecord buildLocalStorageRecord( + Connection connection, StorageId storageId + ) throws SQLException { + Type storageIdType = ManifestRecord.Identifier.Type.fromValue(storageId.getType()); + if (storageIdType == null) { + throw new AssertionError("Got unknown local storage record type: " + storageId); + } + return switch (storageIdType) { + case CONTACT -> { + final var recipient = account.getRecipientStore().getRecipient(connection, storageId); + final var address = recipient.getAddress().getIdentifier(); + final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); + yield StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); + } + case GROUPV1 -> { + final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId); + yield StorageSyncModels.localToRemoteRecord(groupV1, storageId.getRaw()); + } + case GROUPV2 -> { + final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId); + yield StorageSyncModels.localToRemoteRecord(groupV2, storageId.getRaw()); + } + case ACCOUNT -> { + final var selfRecipient = account.getRecipientStore().getRecipient(connection, + account.getSelfRecipientId()); + yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), selfRecipient, + account.getUsernameLink(), storageId.getRaw()); + } + default -> throw new AssertionError("Got unknown local storage record type: " + storageId); + }; + } + + /** + * Given a list of all the local and remote keys you know about, this will + * return a result telling + * you which keys are exclusively remote and which are exclusively local. + * + * @param remoteIds All remote keys available. + * @param localIds All local keys available. + * @return An object describing which keys are exclusive to the remote data set + * and which keys are + * exclusive to the local data set. + */ + private static IdDifferenceResult findIdDifference( + Collection remoteIds, Collection localIds + ) { + final var base64Encoder = Base64.getEncoder(); + final var remoteByRawId = remoteIds.stream() + .collect(Collectors.toMap(id -> base64Encoder.encodeToString(id.getRaw()), id -> id)); + final var localByRawId = localIds.stream() + .collect(Collectors.toMap(id -> base64Encoder.encodeToString(id.getRaw()), id -> id)); + + boolean hasTypeMismatch = remoteByRawId.size() != remoteIds.size() || localByRawId.size() != localIds.size(); + + final var remoteOnlyRawIds = SetUtil.difference(remoteByRawId.keySet(), localByRawId.keySet()); + final var localOnlyRawIds = SetUtil.difference(localByRawId.keySet(), remoteByRawId.keySet()); + final var sharedRawIds = SetUtil.intersection(localByRawId.keySet(), remoteByRawId.keySet()); + + for (String rawId : sharedRawIds) { + final var remote = remoteByRawId.get(rawId); + final var local = localByRawId.get(rawId); + + if (remote.getType() != local.getType() && local.getType() != 0) { + remoteOnlyRawIds.remove(rawId); + localOnlyRawIds.remove(rawId); + hasTypeMismatch = true; + logger.debug("Remote type {} did not match local type {} for {}!", + remote.getType(), + local.getType(), + rawId); + } + } + + final var remoteOnlyKeys = remoteOnlyRawIds.stream().map(remoteByRawId::get).toList(); + final var localOnlyKeys = localOnlyRawIds.stream().map(localByRawId::get).toList(); + + return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch); + } + + private List processKnownRecords( + final Connection connection, List records + ) throws SQLException { + final var unknownRecords = new ArrayList(); + + final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor()); + final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor()); + final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection); + final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection); + + for (final var record : records) { + logger.debug("Reading record of type {}", record.getType()); + Type recordType = ManifestRecord.Identifier.Type.fromValue(record.getType()); + if (recordType == null) { + unknownRecords.add(record.getId()); + } else { + switch (recordType) { + case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get()); + case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get()); + case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get()); + case CONTACT -> contactRecordProcessor.process(record.getContact().get()); + default -> unknownRecords.add(record.getId()); + } + } + } + + return unknownRecords; + } + + /** + * hasTypeMismatches is True if there exist some keys that have matching raw ID's but different types, otherwise false. + */ + private record IdDifferenceResult( + List remoteOnlyIds, List localOnlyIds, boolean hasTypeMismatches + ) { + + public boolean isEmpty() { + return remoteOnlyIds.isEmpty() && localOnlyIds.isEmpty(); + } + } + + private static class RetryLaterException extends Throwable {} } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SyncHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SyncHelper.java index 1be5a26822f09..4789790eb109c 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -2,14 +2,17 @@ import org.asamk.signal.manager.api.Contact; import org.asamk.signal.manager.api.GroupId; +import org.asamk.signal.manager.api.MessageEnvelope.Sync.MessageRequestResponse; import org.asamk.signal.manager.api.TrustLevel; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.StickerPack; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.MimeUtils; +import org.jetbrains.annotations.NotNull; import org.signal.libsignal.protocol.IdentityKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,6 +29,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage; +import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; @@ -47,7 +51,7 @@ public class SyncHelper { - private final static Logger logger = LoggerFactory.getLogger(SyncHelper.class); + private static final Logger logger = LoggerFactory.getLogger(SyncHelper.class); private final Context context; private final SignalAccount account; @@ -79,6 +83,11 @@ public SendMessageResult sendSyncFetchProfileMessage() { .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); } + public void sendSyncFetchStorageMessage() { + context.getSendHelper() + .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST)); + } + public void sendGroups() throws IOException { var groupsFile = IOUtils.createTempFile(); @@ -133,42 +142,17 @@ public void sendContacts() throws IOException { for (var contactPair : account.getContactStore().getContacts()) { final var recipientId = contactPair.first(); final var contact = contactPair.second(); - final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId); - - var currentIdentity = account.getIdentityKeyStore().getIdentityInfo(address.getServiceId()); - VerifiedMessage verifiedMessage = null; - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(address, - currentIdentity.getIdentityKey(), - currentIdentity.getTrustLevel().toVerifiedState(), - currentIdentity.getDateAddedTimestamp()); - } + final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); - var profileKey = account.getProfileStore().getProfileKey(recipientId); - out.write(new DeviceContact(address, - Optional.ofNullable(contact.getName()), - createContactAvatarAttachment(new RecipientAddress(address)), - Optional.ofNullable(contact.getColor()), - Optional.ofNullable(verifiedMessage), - Optional.ofNullable(profileKey), - contact.isBlocked(), - Optional.of(contact.getMessageExpirationTime()), - Optional.empty(), - contact.isArchived())); + out.write(getDeviceContact(address, recipientId, contact)); } if (account.getProfileKey() != null) { // Send our own profile key as well - out.write(new DeviceContact(account.getSelfAddress(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.of(account.getProfileKey()), - false, - Optional.empty(), - Optional.empty(), - false)); + final var address = account.getSelfRecipientAddress(); + final var recipientId = account.getSelfRecipientId(); + final var contact = account.getContactStore().getContact(recipientId); + out.write(getDeviceContact(address, recipientId, contact)); } } @@ -194,6 +178,35 @@ public void sendContacts() throws IOException { } } + @NotNull + private DeviceContact getDeviceContact( + final RecipientAddress address, final RecipientId recipientId, final Contact contact + ) throws IOException { + var currentIdentity = address.serviceId().isEmpty() + ? null + : account.getIdentityKeyStore().getIdentityInfo(address.serviceId().get()); + VerifiedMessage verifiedMessage = null; + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(address.toSignalServiceAddress(), + currentIdentity.getIdentityKey(), + currentIdentity.getTrustLevel().toVerifiedState(), + currentIdentity.getDateAddedTimestamp()); + } + + var profileKey = account.getProfileStore().getProfileKey(recipientId); + return new DeviceContact(address.aci(), + address.number(), + Optional.ofNullable(contact == null ? null : contact.getName()), + createContactAvatarAttachment(address), + Optional.ofNullable(contact == null ? null : contact.color()), + Optional.ofNullable(verifiedMessage), + Optional.ofNullable(profileKey), + contact != null && contact.isBlocked(), + Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()), + Optional.empty(), + contact != null && contact.isArchived()); + } + public SendMessageResult sendBlockedList() { var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { @@ -222,7 +235,7 @@ public SendMessageResult sendVerifiedMessage( } public SendMessageResult sendKeysMessage() { - var keysMessage = new KeysMessage(Optional.ofNullable(account.getStorageKey()), + var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()), Optional.ofNullable(account.getOrCreatePinMasterKey())); return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); } @@ -310,16 +323,21 @@ public void handleSyncDeviceContacts(final InputStream input) throws IOException throw e; } } - if (c == null) { + if (c == null || (c.getAci().isEmpty() && c.getE164().isEmpty())) { break; } - if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { + final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty()); + if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } - final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(c.getAddress()); + final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address); var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - if (c.getName().isPresent()) { + if (c.getName().isPresent() && ( + contact == null || ( + contact.givenName() == null && contact.familyName() == null + ) + )) { builder.withGivenName(c.getName().get()); builder.withFamilyName(null); } @@ -339,16 +357,39 @@ public void handleSyncDeviceContacts(final InputStream input) throws IOException if (c.getExpirationTimer().isPresent()) { builder.withMessageExpirationTime(c.getExpirationTimer().get()); } - builder.withBlocked(c.isBlocked()); - builder.withArchived(c.isArchived()); + builder.withIsBlocked(c.isBlocked()); + builder.withIsArchived(c.isArchived()); account.getContactStore().storeContact(recipientId, builder.build()); if (c.getAvatar().isPresent()) { - downloadContactAvatar(c.getAvatar().get(), new RecipientAddress(c.getAddress())); + downloadContactAvatar(c.getAvatar().get(), address); } } } + public SendMessageResult sendMessageRequestResponse( + final MessageRequestResponse.Type type, final GroupId groupId + ) { + final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type)); + return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response)); + } + + public SendMessageResult sendMessageRequestResponse( + final MessageRequestResponse.Type type, final RecipientId recipientId + ) { + final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId); + if (address.serviceId().isEmpty()) { + return null; + } + context.getContactHelper() + .setContactProfileSharing(recipientId, + type == MessageRequestResponse.Type.ACCEPT + || type == MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT); + final var response = MessageRequestResponseMessage.forIndividual(address.serviceId().get(), + localToRemoteType(type)); + return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response)); + } + private SendMessageResult requestSyncData(final SyncMessage.Request.Type type) { var r = new SyncMessage.Request.Builder().type(type).build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); @@ -373,4 +414,17 @@ private void downloadContactAvatar(SignalServiceAttachment avatar, RecipientAddr logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); } } + + private MessageRequestResponseMessage.Type localToRemoteType(final MessageRequestResponse.Type type) { + return switch (type) { + case UNKNOWN -> MessageRequestResponseMessage.Type.UNKNOWN; + case ACCEPT -> MessageRequestResponseMessage.Type.ACCEPT; + case DELETE -> MessageRequestResponseMessage.Type.DELETE; + case BLOCK -> MessageRequestResponseMessage.Type.BLOCK; + case BLOCK_AND_DELETE -> MessageRequestResponseMessage.Type.BLOCK_AND_DELETE; + case UNBLOCK_AND_ACCEPT -> MessageRequestResponseMessage.Type.UNBLOCK_AND_ACCEPT; + case SPAM -> MessageRequestResponseMessage.Type.SPAM; + case BLOCK_AND_SPAM -> MessageRequestResponseMessage.Type.BLOCK_AND_SPAM; + }; + } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index bc02e319d2216..7c73c80f67e70 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -20,8 +20,8 @@ public class UnidentifiedAccessHelper { - private final static Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class); - private final static long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1); + private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class); + private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1); private static final byte[] UNRESTRICTED_KEY = new byte[16]; private final SignalAccount account; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/JobExecutor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/JobExecutor.java index 8a3e815f96e21..bb6b5e6abfbd8 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/JobExecutor.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/JobExecutor.java @@ -2,16 +2,88 @@ import org.asamk.signal.manager.helper.Context; import org.asamk.signal.manager.jobs.Job; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class JobExecutor { +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +public class JobExecutor implements AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(JobExecutor.class); private final Context context; + private final ExecutorService executorService; + private Job running; + private final Queue queue = new ArrayDeque<>(); public JobExecutor(final Context context) { this.context = context; + this.executorService = Executors.newCachedThreadPool(); } public void enqueueJob(Job job) { - job.run(context); + if (executorService.isShutdown()) { + logger.debug("Not enqueuing {} job, shutting down", job.getClass().getSimpleName()); + return; + } + + synchronized (queue) { + logger.trace("Enqueuing {} job", job.getClass().getSimpleName()); + queue.add(job); + } + + runNextJob(); + } + + private void runNextJob() { + Job job; + synchronized (queue) { + if (running != null) { + return; + } + job = queue.poll(); + running = job; + } + + if (job == null) { + synchronized (this) { + this.notifyAll(); + } + return; + } + logger.debug("Running {} job", job.getClass().getSimpleName()); + executorService.execute(() -> { + try { + job.run(context); + } catch (Throwable e) { + logger.warn("Job {} failed", job.getClass().getSimpleName(), e); + } finally { + synchronized (queue) { + running = null; + } + runNextJob(); + } + }); + } + + @Override + public void close() { + final boolean queueEmpty; + synchronized (queue) { + queueEmpty = queue.isEmpty(); + } + if (queueEmpty) { + executorService.shutdown(); + return; + } + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException ignored) { + } + } + executorService.shutdown(); } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/LibSignalLogger.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/LibSignalLogger.java index 66a480e987175..541ad099edace 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/LibSignalLogger.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/LibSignalLogger.java @@ -7,7 +7,7 @@ public class LibSignalLogger implements SignalProtocolLogger { - private final static Logger logger = LoggerFactory.getLogger("LibSignal"); + private static final Logger logger = LoggerFactory.getLogger("LibSignal"); public static void initLogger() { SignalProtocolLoggerProvider.setProvider(new LibSignalLogger()); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ManagerImpl.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ManagerImpl.java index 216342363da23..eb8513224b49f 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -38,11 +38,13 @@ import org.asamk.signal.manager.api.LastGroupAdminException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.MessageEnvelope; +import org.asamk.signal.manager.api.MessageEnvelope.Sync.MessageRequestResponse; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NotAGroupMemberException; import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.PendingAdminApprovalException; +import org.asamk.signal.manager.api.PhoneNumberSharingMode; import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.api.RateLimitException; @@ -61,15 +63,19 @@ import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UserStatus; +import org.asamk.signal.manager.api.UsernameLinkUrl; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.helper.AccountFileUpdater; import org.asamk.signal.manager.helper.Context; import org.asamk.signal.manager.helper.RecipientHelper.RegisteredUser; +import org.asamk.signal.manager.jobs.RefreshRecipientsJob; +import org.asamk.signal.manager.jobs.SyncStorageJob; import org.asamk.signal.manager.storage.AttachmentStore; import org.asamk.signal.manager.storage.AvatarStore; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack; import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore; @@ -100,6 +106,7 @@ import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -123,10 +130,11 @@ import java.util.stream.Stream; import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.schedulers.Schedulers; public class ManagerImpl implements Manager { - private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); + private static final Logger logger = LoggerFactory.getLogger(ManagerImpl.class); private SignalAccount account; private final SignalDependencies dependencies; @@ -191,22 +199,25 @@ public void removeAccount() { this.notifyAll(); } }); - disposable.add(account.getIdentityKeyStore().getIdentityChanges().subscribe(serviceId -> { - logger.trace("Archiving old sessions for {}", serviceId); - account.getAccountData(ServiceIdType.ACI).getSessionStore().archiveSessions(serviceId); - account.getAccountData(ServiceIdType.PNI).getSessionStore().archiveSessions(serviceId); - account.getSenderKeyStore().deleteSharedWith(serviceId); - final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); - final var profile = account.getProfileStore().getProfile(recipientId); - if (profile != null) { - account.getProfileStore() - .storeProfile(recipientId, - Profile.newBuilder(profile) - .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN) - .withLastUpdateTimestamp(0) - .build()); - } - })); + disposable.add(account.getIdentityKeyStore() + .getIdentityChanges() + .observeOn(Schedulers.from(executor)) + .subscribe(serviceId -> { + logger.trace("Archiving old sessions for {}", serviceId); + account.getAccountData(ServiceIdType.ACI).getSessionStore().archiveSessions(serviceId); + account.getAccountData(ServiceIdType.PNI).getSessionStore().archiveSessions(serviceId); + account.getSenderKeyStore().deleteSharedWith(serviceId); + final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); + final var profile = account.getProfileStore().getProfile(recipientId); + if (profile != null) { + account.getProfileStore() + .storeProfile(recipientId, + Profile.newBuilder(profile) + .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN) + .withLastUpdateTimestamp(0) + .build()); + } + })); } @Override @@ -219,14 +230,7 @@ public void checkAccountState() throws IOException { final var lastRecipientsRefresh = account.getLastRecipientsRefresh(); if (lastRecipientsRefresh == null || lastRecipientsRefresh < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) { - try { - context.getRecipientHelper().refreshUsers(); - } catch (Exception e) { - logger.warn("Full CDSI recipients refresh failed, ignoring: {} ({})", - e.getMessage(), - e.getClass().getSimpleName()); - logger.debug("Full CDSI refresh failed", e); - } + context.getJobExecutor().enqueueJob(new RefreshRecipientsJob()); context.getAccountHelper().checkWhoAmiI(); } } @@ -275,10 +279,27 @@ public Map getUserStatus(Set numbers) throws IOExcep } @Override - public void updateAccountAttributes(String deviceName) throws IOException { + public void updateAccountAttributes( + String deviceName, + Boolean unrestrictedUnidentifiedSender, + final Boolean discoverableByNumber, + final Boolean numberSharing + ) throws IOException { if (deviceName != null) { context.getAccountHelper().setDeviceName(deviceName); } + if (unrestrictedUnidentifiedSender != null) { + account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedSender); + } + if (discoverableByNumber != null) { + account.getConfigurationStore().setPhoneNumberUnlisted(!discoverableByNumber); + } + if (numberSharing != null) { + account.getConfigurationStore() + .setPhoneNumberSharingMode(numberSharing + ? PhoneNumberSharingMode.EVERYBODY + : PhoneNumberSharingMode.NOBODY); + } context.getAccountHelper().updateAccountAttributes(); context.getAccountHelper().checkWhoAmiI(); } @@ -290,13 +311,7 @@ public Configuration getConfiguration() { } @Override - public void updateConfiguration( - Configuration configuration - ) throws NotPrimaryDeviceException { - if (!account.isPrimaryDevice()) { - throw new NotPrimaryDeviceException(); - } - + public void updateConfiguration(Configuration configuration) { final var configurationStore = account.getConfigurationStore(); if (configuration.readReceipts().isPresent()) { configurationStore.setReadReceipts(configuration.readReceipts().get()); @@ -311,6 +326,7 @@ public void updateConfiguration( configurationStore.setLinkPreviews(configuration.linkPreviews().get()); } context.getSyncHelper().sendConfigurationMessage(); + syncRemoteStorage(); } @Override @@ -332,9 +348,19 @@ void refreshCurrentUsername() throws IOException, BaseUsernameException { } @Override - public String setUsername(final String username) throws IOException, InvalidUsernameException { + public String getUsername() { + return account.getUsername(); + } + + @Override + public UsernameLinkUrl getUsernameLink() { + return new UsernameLinkUrl(account.getUsernameLink()); + } + + @Override + public void setUsername(final String username) throws IOException, InvalidUsernameException { try { - return context.getAccountHelper().reserveUsername(username); + context.getAccountHelper().reserveUsername(username); } catch (BaseUsernameException e) { throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e); } @@ -517,21 +543,31 @@ public Pair joinGroup( } private SendMessageResults sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipients + SignalServiceDataMessage.Builder messageBuilder, Set recipients, boolean notifySelf ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - return sendMessage(messageBuilder, recipients, Optional.empty()); + return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty()); } private SendMessageResults sendMessage( SignalServiceDataMessage.Builder messageBuilder, Set recipients, + boolean notifySelf, Optional editTargetTimestamp ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var results = new HashMap>(); long timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); for (final var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single single) { + if (recipient instanceof RecipientIdentifier.NoteToSelf || ( + recipient instanceof RecipientIdentifier.Single single + && new RecipientAddress(single.toPartialRecipientAddress()).matches(account.getSelfRecipientAddress()) + )) { + final var result = notifySelf + ? context.getSendHelper() + .sendMessage(messageBuilder, account.getSelfRecipientId(), editTargetTimestamp) + : context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp); + results.put(recipient, List.of(toSendMessageResult(result))); + } else if (recipient instanceof RecipientIdentifier.Single single) { try { final var recipientId = context.getRecipientHelper().resolveRecipient(single); final var result = context.getSendHelper() @@ -541,12 +577,9 @@ private SendMessageResults sendMessage( results.put(recipient, List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); } - } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { - final var result = context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp); - results.put(recipient, List.of(toSendMessageResult(result))); } else if (recipient instanceof RecipientIdentifier.Group group) { final var result = context.getSendHelper() - .sendAsGroupMessage(messageBuilder, group.groupId(), editTargetTimestamp); + .sendAsGroupMessage(messageBuilder, group.groupId(), notifySelf, editTargetTimestamp); results.put(recipient, result.stream().map(this::toSendMessageResult).toList()); } } @@ -631,7 +664,7 @@ private SendMessageResults sendReceiptMessage( @Override public SendMessageResults sendMessage( - Message message, Set recipients + Message message, Set recipients, boolean notifySelf ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { final var selfProfile = context.getProfileHelper().getSelfProfile(); if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) { @@ -640,7 +673,7 @@ public SendMessageResults sendMessage( } final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); - return sendMessage(messageBuilder, recipients); + return sendMessage(messageBuilder, recipients, notifySelf); } @Override @@ -649,7 +682,7 @@ public SendMessageResults sendEditMessage( ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); - return sendMessage(messageBuilder, recipients, Optional.of(editTargetTimestamp)); + return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp)); } private void applyMessage( @@ -774,7 +807,7 @@ public SendMessageResults sendRemoteDeleteMessage( account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, r.groupId()); } } - return sendMessage(messageBuilder, recipients); + return sendMessage(messageBuilder, recipients, false); } @Override @@ -796,7 +829,7 @@ public SendMessageResults sendMessageReaction( messageBuilder.withStoryContext(new SignalServiceDataMessage.StoryContext(authorServiceId, targetSentTimestamp)); } - return sendMessage(messageBuilder, recipients); + return sendMessage(messageBuilder, recipients, false); } @Override @@ -807,7 +840,7 @@ public SendMessageResults sendPaymentNotificationMessage( final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null); final var messageBuilder = SignalServiceDataMessage.newBuilder().withPayment(payment); try { - return sendMessage(messageBuilder, Set.of(recipient)); + return sendMessage(messageBuilder, Set.of(recipient), false); } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } @@ -819,7 +852,8 @@ public SendMessageResults sendEndSessionMessage(Set try { return sendMessage(messageBuilder, - recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); + recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()), + false); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } finally { @@ -841,11 +875,57 @@ public SendMessageResults sendEndSessionMessage(Set } } + @Override + public SendMessageResults sendMessageRequestResponse( + final MessageRequestResponse.Type type, final Set recipients + ) { + var results = new HashMap>(); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.NoteToSelf || ( + recipient instanceof RecipientIdentifier.Single single + && new RecipientAddress(single.toPartialRecipientAddress()).matches(account.getSelfRecipientAddress()) + )) { + final var result = context.getSyncHelper() + .sendMessageRequestResponse(type, account.getSelfRecipientId()); + if (result != null) { + results.put(recipient, List.of(toSendMessageResult(result))); + } + results.put(recipient, List.of(toSendMessageResult(result))); + } else if (recipient instanceof RecipientIdentifier.Single single) { + try { + final var recipientId = context.getRecipientHelper().resolveRecipient(single); + final var result = context.getSyncHelper().sendMessageRequestResponse(type, recipientId); + if (result != null) { + results.put(recipient, List.of(toSendMessageResult(result))); + } + } catch (UnregisteredRecipientException e) { + results.put(recipient, + List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); + } + } else if (recipient instanceof RecipientIdentifier.Group group) { + final var result = context.getSyncHelper().sendMessageRequestResponse(type, group.groupId()); + results.put(recipient, List.of(toSendMessageResult(result))); + } + } + return new SendMessageResults(0, results); + } + + @Override + public void hideRecipient(final RecipientIdentifier.Single recipient) { + final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient); + if (recipientIdOptional.isPresent()) { + context.getContactHelper().setContactHidden(recipientIdOptional.get(), true); + account.removeRecipient(recipientIdOptional.get()); + syncRemoteStorage(); + } + } + @Override public void deleteRecipient(final RecipientIdentifier.Single recipient) { final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient); if (recipientIdOptional.isPresent()) { account.removeRecipient(recipientIdOptional.get()); + syncRemoteStorage(); } } @@ -854,6 +934,7 @@ public void deleteContact(final RecipientIdentifier.Single recipient) { final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient); if (recipientIdOptional.isPresent()) { account.getContactStore().deleteContact(recipientIdOptional.get()); + syncRemoteStorage(); } } @@ -866,15 +947,13 @@ public void setContactName( } context.getContactHelper() .setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName); + syncRemoteStorage(); } @Override public void setContactsBlocked( Collection recipients, boolean blocked - ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException { - if (!account.isPrimaryDevice()) { - throw new NotPrimaryDeviceException(); - } + ) throws IOException, UnregisteredRecipientException { if (recipients.isEmpty()) { return; } @@ -886,6 +965,10 @@ public void setContactsBlocked( continue; } context.getContactHelper().setContactBlocked(recipientId, blocked); + context.getSyncHelper() + .sendMessageRequestResponse(blocked + ? MessageRequestResponse.Type.BLOCK + : MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT, recipientId); // if we don't have a common group with the blocked contact we need to rotate the profile key shouldRotateProfileKey = blocked && ( shouldRotateProfileKey || account.getGroupStore() @@ -898,15 +981,13 @@ public void setContactsBlocked( context.getProfileHelper().rotateProfileKey(); } context.getSyncHelper().sendBlockedList(); + syncRemoteStorage(); } @Override public void setGroupsBlocked( final Collection groupIds, final boolean blocked - ) throws GroupNotFoundException, NotPrimaryDeviceException, IOException { - if (!account.isPrimaryDevice()) { - throw new NotPrimaryDeviceException(); - } + ) throws GroupNotFoundException, IOException { if (groupIds.isEmpty()) { return; } @@ -916,12 +997,17 @@ public void setGroupsBlocked( continue; } context.getGroupHelper().setGroupBlocked(groupId, blocked); + context.getSyncHelper() + .sendMessageRequestResponse(blocked + ? MessageRequestResponse.Type.BLOCK + : MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT, groupId); shouldRotateProfileKey = blocked; } if (shouldRotateProfileKey) { context.getProfileHelper().rotateProfileKey(); } context.getSyncHelper().sendBlockedList(); + syncRemoteStorage(); } @Override @@ -932,10 +1018,11 @@ public void setExpirationTimer( context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer); final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { - sendMessage(messageBuilder, Set.of(recipient)); + sendMessage(messageBuilder, Set.of(recipient), false); } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } + syncRemoteStorage(); } @Override @@ -993,13 +1080,13 @@ public List getStickerPacks() { } @Override - public void requestAllSyncData() throws IOException { + public void requestAllSyncData() { context.getSyncHelper().requestAllSyncData(); - retrieveRemoteStorage(); + syncRemoteStorage(); } - void retrieveRemoteStorage() throws IOException { - context.getStorageHelper().readDataFromStorage(); + void syncRemoteStorage() { + context.getJobExecutor().enqueueJob(new SyncStorageJob()); } @Override @@ -1033,9 +1120,7 @@ private void startReceiveThreadIfRequired() { startReceiveThreadIfRequired(); } } - }); - receiveThread.setName("receive-" + threadNumber.getAndIncrement()); - + }, "receive-" + threadNumber.getAndIncrement()); receiveThread.start(); } @@ -1095,6 +1180,20 @@ public void receiveMessages( receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler); } + @Override + public void stopReceiveMessages() { + Thread thread = null; + synchronized (messageHandlers) { + if (isReceivingSynchronous) { + thread = receiveThread; + receiveThread = null; + } + } + if (thread != null) { + stopReceiveThread(thread); + } + } + private void receiveMessages( Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler ) throws IOException, AlreadyReceivingException { @@ -1302,6 +1401,58 @@ public InputStream retrieveAttachment(final String id) throws IOException { return context.getAttachmentHelper().retrieveAttachment(id).getStream(); } + @Override + public InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + final var recipientId = context.getRecipientHelper().resolveRecipient(recipient); + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address); + if (streamDetails == null) { + throw new FileNotFoundException(); + } + return streamDetails.getStream(); + } + + @Override + public InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + final var recipientId = context.getRecipientHelper().resolveRecipient(recipient); + context.getProfileHelper().getRecipientProfile(recipientId); + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + final var streamDetails = context.getAvatarStore().retrieveProfileAvatar(address); + if (streamDetails == null) { + throw new FileNotFoundException(); + } + return streamDetails.getStream(); + } + + @Override + public InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException { + final var streamDetails = context.getAvatarStore().retrieveGroupAvatar(groupId); + context.getGroupHelper().getGroup(groupId); + if (streamDetails == null) { + throw new FileNotFoundException(); + } + return streamDetails.getStream(); + } + + @Override + public InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException { + var streamDetails = context.getStickerPackStore().retrieveSticker(stickerPackId, stickerId); + if (streamDetails == null) { + final var pack = account.getStickerStore().getStickerPack(stickerPackId); + if (pack != null) { + try { + context.getStickerHelper().retrieveStickerPack(stickerPackId, pack.packKey()); + } catch (InvalidMessageException e) { + logger.warn("Failed to download sticker pack"); + } + } + } + if (streamDetails == null) { + throw new FileNotFoundException(); + } + return streamDetails.getStream(); + } + @Override public void close() { Thread thread; @@ -1314,6 +1465,7 @@ public void close() { if (thread != null) { stopReceiveThread(thread); } + context.close(); executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java index 4dc1ede289999..478e761bea037 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java @@ -22,7 +22,7 @@ public class MultiAccountManagerImpl implements MultiAccountManager { - private final static Logger logger = LoggerFactory.getLogger(MultiAccountManagerImpl.class); + private static final Logger logger = LoggerFactory.getLogger(MultiAccountManagerImpl.class); private final Set> onManagerAddedHandlers = new HashSet<>(); private final Set> onManagerRemovedHandlers = new HashSet<>(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java index e58d3faf49264..1dbe7e9b6603f 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java @@ -48,7 +48,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager { - private final static Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class); + private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class); private final PathConfig pathConfig; private final ServiceEnvironmentConfig serviceEnvironmentConfig; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java index f455275946395..4518fb01c86ef 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java @@ -39,12 +39,14 @@ import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; +import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; @@ -55,7 +57,7 @@ public class RegistrationManagerImpl implements RegistrationManager { - private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class); + private static final Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class); private SignalAccount account; private final PathConfig pathConfig; @@ -92,7 +94,10 @@ public RegistrationManagerImpl( userAgent, groupsV2Operations, ServiceConfig.AUTOMATIC_NETWORK_RETRY); - final var secureValueRecoveryV2 = accountManager.getSecureValueRecoveryV2(serviceEnvironmentConfig.svr2Mrenclave()); + final var secureValueRecoveryV2 = serviceEnvironmentConfig.svr2Mrenclaves() + .stream() + .map(mr -> (SecureValueRecovery) accountManager.getSecureValueRecoveryV2(mr)) + .toList(); this.pinHelper = new PinHelper(secureValueRecoveryV2); } @@ -107,6 +112,11 @@ public void register( } try { + final var recoveryPassword = account.getRecoveryPassword(); + if (recoveryPassword != null && account.isPrimaryDevice() && attemptReregisterAccount(recoveryPassword)) { + return; + } + if (account.getAci() != null && attemptReactivateAccount()) { return; } @@ -152,43 +162,7 @@ public void verifyAccount( pin = null; } - //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); - final var aci = ACI.parseOrThrow(response.getUuid()); - final var pni = PNI.parseOrThrow(response.getPni()); - account.finishRegistration(aci, pni, masterKey, pin, aciPreKeys, pniPreKeys); - accountFileUpdater.updateAccountIdentifiers(account.getNumber(), aci); - - ManagerImpl m = null; - try { - m = new ManagerImpl(account, pathConfig, accountFileUpdater, serviceEnvironmentConfig, userAgent); - account = null; - - m.refreshPreKeys(); - if (response.isStorageCapable()) { - m.retrieveRemoteStorage(); - } - // Set an initial empty profile so user can be added to groups - try { - m.updateProfile(UpdateProfile.newBuilder().build()); - } catch (NoClassDefFoundError e) { - logger.warn("Failed to set default profile: {}", e.getMessage()); - } - - try { - m.refreshCurrentUsername(); - } catch (IOException | BaseUsernameException e) { - logger.warn("Failed to refresh current username", e); - } - - if (newManagerListener != null) { - newManagerListener.accept(m); - m = null; - } - } finally { - if (m != null) { - m.close(); - } - } + finishAccountRegistration(response, pin, masterKey, aciPreKeys, pniPreKeys); } @Override @@ -203,6 +177,34 @@ public boolean isRegistered() { return account.isRegistered(); } + private boolean attemptReregisterAccount(final String recoveryPassword) { + try { + if (account.getPniIdentityKeyPair() == null) { + account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair()); + } + + final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI)); + final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); + final var response = Utils.handleResponseException(accountManager.registerAccount(null, + recoveryPassword, + account.getAccountAttributes(null), + aciPreKeys, + pniPreKeys, + null, + true)); + finishAccountRegistration(response, + account.getRegistrationLockPin(), + account.getPinBackedMasterKey(), + aciPreKeys, + pniPreKeys); + logger.info("Reregistered existing account, verify is not necessary."); + return true; + } catch (IOException e) { + logger.debug("Failed to reregister account with recovery password", e); + } + return false; + } + private boolean attemptReactivateAccount() { try { final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.signalServiceConfiguration(), @@ -250,6 +252,52 @@ private VerifyAccountResponse verifyAccountWithCode( true)); } + private void finishAccountRegistration( + final VerifyAccountResponse response, + final String pin, + final MasterKey masterKey, + final PreKeyCollection aciPreKeys, + final PreKeyCollection pniPreKeys + ) throws IOException { + //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); + final var aci = ACI.parseOrThrow(response.getUuid()); + final var pni = PNI.parseOrThrow(response.getPni()); + account.finishRegistration(aci, pni, masterKey, pin, aciPreKeys, pniPreKeys); + accountFileUpdater.updateAccountIdentifiers(account.getNumber(), aci); + + ManagerImpl m = null; + try { + m = new ManagerImpl(account, pathConfig, accountFileUpdater, serviceEnvironmentConfig, userAgent); + account = null; + + m.refreshPreKeys(); + if (response.isStorageCapable()) { + m.syncRemoteStorage(); + } + // Set an initial empty profile so user can be added to groups + try { + m.updateProfile(UpdateProfile.newBuilder().build()); + } catch (NoClassDefFoundError e) { + logger.warn("Failed to set default profile: {}", e.getMessage()); + } + + try { + m.refreshCurrentUsername(); + } catch (IOException | BaseUsernameException e) { + logger.warn("Failed to refresh current username", e); + } + + if (newManagerListener != null) { + newManagerListener.accept(m); + m = null; + } + } finally { + if (m != null) { + m.close(); + } + } + } + @Override public void close() { if (account != null) { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalDependencies.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalDependencies.java index 8b5f3e269d0df..9488c75e11a86 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -14,15 +14,17 @@ import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.services.ProfileService; -import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2; +import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.websocket.WebSocketFactory; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.websocket.WebSocketConnection; +import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.function.Supplier; @@ -50,9 +52,8 @@ public class SignalDependencies { private SignalServiceMessageReceiver messageReceiver; private SignalServiceMessageSender messageSender; - private SecureValueRecoveryV2 secureValueRecoveryV2; + private List secureValueRecoveryV2; private ProfileService profileService; - private SignalServiceCipher cipher; SignalDependencies( final ServiceEnvironmentConfig serviceEnvironmentConfig, @@ -76,7 +77,6 @@ public void resetAfterAddressChange() { this.pushServiceSocket = null; } this.messageSender = null; - this.cipher = null; getSignalWebSocket().forceNewWebSockets(); } @@ -192,9 +192,12 @@ public SignalServiceMessageSender getMessageSender() { pushServiceSocket)); } - public SecureValueRecoveryV2 getSecureValueRecoveryV2() { + public List getSecureValueRecoveryV2() { return getOrCreate(() -> secureValueRecoveryV2, - () -> secureValueRecoveryV2 = getAccountManager().getSecureValueRecoveryV2(serviceEnvironmentConfig.svr2Mrenclave())); + () -> secureValueRecoveryV2 = serviceEnvironmentConfig.svr2Mrenclaves() + .stream() + .map(mr -> (SecureValueRecovery) getAccountManager().getSecureValueRecoveryV2(mr)) + .toList()); } public ProfileService getProfileService() { @@ -204,13 +207,15 @@ public ProfileService getProfileService() { getSignalWebSocket())); } - public SignalServiceCipher getCipher() { - return getOrCreate(() -> cipher, () -> { - final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoot()); - final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164()); - final var deviceId = credentialsProvider.getDeviceId(); - cipher = new SignalServiceCipher(address, deviceId, dataStore.aci(), sessionLock, certificateValidator); - }); + public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { + final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoot()); + final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164()); + final var deviceId = credentialsProvider.getDeviceId(); + return new SignalServiceCipher(address, + deviceId, + serviceIdType == ServiceIdType.ACI ? dataStore.aci() : dataStore.pni(), + sessionLock, + certificateValidator); } private T getOrCreate(Supplier supplier, Callable creator) { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 0986d62e5d8c7..02bdd49ea52f2 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -23,7 +23,7 @@ */ final class SignalWebSocketHealthMonitor implements HealthMonitor { - private final static Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class); + private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class); private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(WebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS); private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3; @@ -157,7 +157,7 @@ public void shutdown() { } } - private final static class HttpErrorTracker { + private static final class HttpErrorTracker { private final long[] timestamps; private final long errorTimeRange; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/CheckWhoAmIJob.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/CheckWhoAmIJob.java new file mode 100644 index 0000000000000..165bf70e9826d --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/CheckWhoAmIJob.java @@ -0,0 +1,22 @@ +package org.asamk.signal.manager.jobs; + +import org.asamk.signal.manager.helper.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class CheckWhoAmIJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(CheckWhoAmIJob.class); + + @Override + public void run(Context context) { + logger.trace("Checking whoAmI"); + try { + context.getAccountHelper().checkWhoAmiI(); + } catch (IOException e) { + logger.warn("Failed to check whoAmI", e); + } + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/CleanOldPreKeysJob.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/CleanOldPreKeysJob.java new file mode 100644 index 0000000000000..d933fb0f74dae --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/CleanOldPreKeysJob.java @@ -0,0 +1,16 @@ +package org.asamk.signal.manager.jobs; + +import org.asamk.signal.manager.helper.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CleanOldPreKeysJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(CleanOldPreKeysJob.class); + + @Override + public void run(Context context) { + logger.trace("Cleaning old prekeys"); + context.getPreKeyHelper().cleanOldPreKeys(); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java new file mode 100644 index 0000000000000..f46bac0c520d6 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java @@ -0,0 +1,23 @@ +package org.asamk.signal.manager.jobs; + +import org.asamk.signal.manager.helper.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DownloadProfileAvatarJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(DownloadProfileAvatarJob.class); + private final String avatarPath; + + public DownloadProfileAvatarJob(final String avatarPath) { + this.avatarPath = avatarPath; + } + + @Override + public void run(Context context) { + logger.trace("Downloading profile avatar {}", avatarPath); + final var account = context.getAccount(); + context.getProfileHelper() + .downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, account.getProfileKey()); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java new file mode 100644 index 0000000000000..2279686422449 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java @@ -0,0 +1,24 @@ +package org.asamk.signal.manager.jobs; + +import org.asamk.signal.manager.helper.Context; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DownloadProfileJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(DownloadProfileJob.class); + private final RecipientAddress address; + + public DownloadProfileJob(RecipientAddress address) { + this.address = address; + } + + @Override + public void run(Context context) { + logger.trace("Refreshing profile for {}", address); + final var account = context.getAccount(); + final var recipientId = account.getRecipientStore().resolveRecipient(address); + context.getProfileHelper().refreshRecipientProfile(recipientId); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/RefreshRecipientsJob.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/RefreshRecipientsJob.java new file mode 100644 index 0000000000000..472bbac1d9406 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/RefreshRecipientsJob.java @@ -0,0 +1,23 @@ +package org.asamk.signal.manager.jobs; + +import org.asamk.signal.manager.helper.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RefreshRecipientsJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(RefreshRecipientsJob.class); + + @Override + public void run(Context context) { + logger.trace("Full CDSI recipients refresh"); + try { + context.getRecipientHelper().refreshUsers(); + } catch (Exception e) { + logger.warn("Full CDSI recipients refresh failed, ignoring: {} ({})", + e.getMessage(), + e.getClass().getSimpleName()); + logger.debug("Full CDSI refresh failed", e); + } + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java index 9d90a5463e018..1eeed9dc02e26 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java @@ -11,7 +11,7 @@ public class RetrieveStickerPackJob implements Job { - private final static Logger logger = LoggerFactory.getLogger(RetrieveStickerPackJob.class); + private static final Logger logger = LoggerFactory.getLogger(RetrieveStickerPackJob.class); private final StickerPackId packId; private final byte[] packKey; @@ -23,6 +23,7 @@ public RetrieveStickerPackJob(final StickerPackId packId, final byte[] packKey) @Override public void run(Context context) { + logger.trace("Downloading sticker pack {}", packId); try { context.getStickerHelper().retrieveStickerPack(packId, packKey); } catch (IOException e) { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/SyncStorageJob.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/SyncStorageJob.java new file mode 100644 index 0000000000000..85cfbe2c2170b --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/jobs/SyncStorageJob.java @@ -0,0 +1,22 @@ +package org.asamk.signal.manager.jobs; + +import org.asamk.signal.manager.helper.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class SyncStorageJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class); + + @Override + public void run(Context context) { + logger.trace("Running storage sync job"); + try { + context.getStorageHelper().syncDataWithStorage(); + } catch (IOException e) { + logger.warn("Failed to sync storage data", e); + } + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/AccountDatabase.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/AccountDatabase.java index e09cd4c2b0c2c..014009c45fc42 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/AccountDatabase.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/AccountDatabase.java @@ -25,14 +25,15 @@ import java.io.File; import java.sql.Connection; import java.sql.SQLException; +import java.sql.Statement; import java.util.HashMap; import java.util.Optional; import java.util.UUID; public class AccountDatabase extends Database { - private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class); - private static final long DATABASE_VERSION = 18; + private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class); + private static final long DATABASE_VERSION = 24; private AccountDatabase(final HikariDataSource dataSource) { super(logger, DATABASE_VERSION, dataSource); @@ -57,6 +58,7 @@ protected void createDatabase(final Connection connection) throws SQLException { SenderKeySharedStore.createSql(connection); KeyValueStore.createSql(connection); CdsiStore.createSql(connection); + UnknownStorageIdStore.createSql(connection); } @Override @@ -360,60 +362,7 @@ CREATE TABLE kyber_pre_key ( if (oldVersion < 15) { logger.debug("Updating database: Store serviceId as TEXT"); try (final var statement = connection.createStatement()) { - statement.executeUpdate(""" - CREATE TABLE tmp_mapping_table ( - uuid BLOB NOT NULL, - address TEXT NOT NULL - ) STRICT; - """); - - final var sql = ( - """ - SELECT r.uuid, r.pni - FROM recipient r - """ - ); - final var uuidAddressMapping = new HashMap(); - try (final var preparedStatement = connection.prepareStatement(sql)) { - try (var result = Utils.executeQueryForStream(preparedStatement, (resultSet) -> { - final var pni = Optional.ofNullable(resultSet.getBytes("pni")) - .map(UuidUtil::parseOrNull) - .map(ServiceId.PNI::from); - final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid")) - .map(UuidUtil::parseOrNull); - final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get() - .equals(pni.get().getRawUuid()) - ? pni.map(p -> p) - : serviceIdUuid.map(ACI::from); - - return new Pair<>(serviceId, pni); - })) { - result.forEach(p -> { - final var serviceId = p.first(); - final var pni = p.second(); - if (serviceId.isPresent()) { - uuidAddressMapping.put(serviceId.get().getRawUuid(), serviceId.get()); - } - if (pni.isPresent()) { - uuidAddressMapping.put(pni.get().getRawUuid(), pni.get()); - } - }); - } - } - - final var insertSql = """ - INSERT INTO tmp_mapping_table (uuid, address) - VALUES (?,?) - """; - try (final var insertStatement = connection.prepareStatement(insertSql)) { - for (final var entry : uuidAddressMapping.entrySet()) { - final var uuid = entry.getKey(); - final var serviceId = entry.getValue(); - insertStatement.setBytes(1, UuidUtil.toByteArray(uuid)); - insertStatement.setString(2, serviceId.toString()); - insertStatement.execute(); - } - } + createUuidMappingTable(connection, statement); statement.executeUpdate(""" CREATE TABLE identity2 ( @@ -531,5 +480,167 @@ CREATE TABLE cdsi ( """); } } + if (oldVersion < 19) { + logger.debug("Updating database: Adding contact hidden column"); + try (final var statement = connection.createStatement()) { + statement.executeUpdate(""" + ALTER TABLE recipient ADD COLUMN hidden INTEGER NOT NULL DEFAULT FALSE; + """); + } + } + if (oldVersion < 20) { + logger.debug("Updating database: Creating storage id tables and columns"); + try (final var statement = connection.createStatement()) { + statement.executeUpdate(""" + CREATE TABLE storage_id ( + _id INTEGER PRIMARY KEY, + type INTEGER NOT NULL, + storage_id BLOB UNIQUE NOT NULL + ) STRICT; + ALTER TABLE group_v1 ADD COLUMN storage_id BLOB; + ALTER TABLE group_v1 ADD COLUMN storage_record BLOB; + ALTER TABLE group_v2 ADD COLUMN storage_id BLOB; + ALTER TABLE group_v2 ADD COLUMN storage_record BLOB; + ALTER TABLE recipient ADD COLUMN storage_id BLOB; + ALTER TABLE recipient ADD COLUMN storage_record BLOB; + """); + } + } + if (oldVersion < 21) { + logger.debug("Updating database: Create unregistered column"); + try (final var statement = connection.createStatement()) { + statement.executeUpdate(""" + ALTER TABLE recipient ADD unregistered_timestamp INTEGER; + """); + } + } + if (oldVersion < 22) { + logger.debug("Updating database: Store recipient aci/pni as TEXT"); + try (final var statement = connection.createStatement()) { + createUuidMappingTable(connection, statement); + + statement.executeUpdate(""" + CREATE TABLE recipient2 ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_id BLOB UNIQUE, + storage_record BLOB, + number TEXT UNIQUE, + username TEXT UNIQUE, + aci TEXT UNIQUE, + pni TEXT UNIQUE, + unregistered_timestamp INTEGER, + profile_key BLOB, + profile_key_credential BLOB, + + given_name TEXT, + family_name TEXT, + nick_name TEXT, + color TEXT, + + expiration_time INTEGER NOT NULL DEFAULT 0, + mute_until INTEGER NOT NULL DEFAULT 0, + blocked INTEGER NOT NULL DEFAULT FALSE, + archived INTEGER NOT NULL DEFAULT FALSE, + profile_sharing INTEGER NOT NULL DEFAULT FALSE, + hide_story INTEGER NOT NULL DEFAULT FALSE, + hidden INTEGER NOT NULL DEFAULT FALSE, + + profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0, + profile_given_name TEXT, + profile_family_name TEXT, + profile_about TEXT, + profile_about_emoji TEXT, + profile_avatar_url_path TEXT, + profile_mobile_coin_address BLOB, + profile_unidentified_access_mode TEXT, + profile_capabilities TEXT + ) STRICT; + INSERT INTO recipient2 (_id, aci, pni, storage_id, storage_record, number, username, unregistered_timestamp, profile_key, profile_key_credential, given_name, family_name, color, expiration_time, blocked, archived, profile_sharing, hidden, profile_last_update_timestamp, profile_given_name, profile_family_name, profile_about, profile_about_emoji, profile_avatar_url_path, profile_mobile_coin_address, profile_unidentified_access_mode, profile_capabilities) + SELECT r._id, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = r.uuid AND t.address not like 'PNI:%') aci, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = r.pni AND t.address like 'PNI:%') pni, storage_id, storage_record, number, username, unregistered_timestamp, profile_key, profile_key_credential, given_name, family_name, color, expiration_time, blocked, archived, profile_sharing, hidden, profile_last_update_timestamp, profile_given_name, profile_family_name, profile_about, profile_about_emoji, profile_avatar_url_path, profile_mobile_coin_address, profile_unidentified_access_mode, profile_capabilities + FROM recipient r; + DROP TABLE recipient; + ALTER TABLE recipient2 RENAME TO recipient; + + DROP TABLE tmp_mapping_table; + """); + } + } + if (oldVersion < 23) { + logger.debug("Updating database: Create group profile sharing column"); + try (final var statement = connection.createStatement()) { + statement.executeUpdate(""" + ALTER TABLE group_v2 ADD profile_sharing INTEGER NOT NULL DEFAULT TRUE; + """); + } + } + if (oldVersion < 24) { + logger.debug("Updating database: Create needs_pni_signature column"); + try (final var statement = connection.createStatement()) { + statement.executeUpdate(""" + ALTER TABLE recipient ADD needs_pni_signature INTEGER NOT NULL DEFAULT FALSE; + """); + } + } + } + + private static void createUuidMappingTable( + final Connection connection, final Statement statement + ) throws SQLException { + statement.executeUpdate(""" + CREATE TABLE tmp_mapping_table ( + uuid BLOB NOT NULL, + address TEXT NOT NULL + ) STRICT; + """); + + final var sql = ( + """ + SELECT r.uuid, r.pni + FROM recipient r + """ + ); + final var uuidAddressMapping = new HashMap(); + try (final var preparedStatement = connection.prepareStatement(sql)) { + try (var result = Utils.executeQueryForStream(preparedStatement, (resultSet) -> { + final var pni = Optional.ofNullable(resultSet.getBytes("pni")) + .map(UuidUtil::parseOrNull) + .map(ServiceId.PNI::from); + final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid")).map(UuidUtil::parseOrNull); + final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get() + .equals(pni.get().getRawUuid()) + ? pni.map(p -> p) + : serviceIdUuid.map(ACI::from); + + return new Pair<>(serviceId, pni); + })) { + result.forEach(p -> { + final var serviceId = p.first(); + final var pni = p.second(); + if (serviceId.isPresent()) { + final var rawUuid = serviceId.get().getRawUuid(); + if (!uuidAddressMapping.containsKey(rawUuid)) { + uuidAddressMapping.put(rawUuid, serviceId.get()); + } + } + if (pni.isPresent()) { + uuidAddressMapping.put(pni.get().getRawUuid(), pni.get()); + } + }); + } + } + + final var insertSql = """ + INSERT INTO tmp_mapping_table (uuid, address) + VALUES (?,?) + """; + try (final var insertStatement = connection.prepareStatement(insertSql)) { + for (final var entry : uuidAddressMapping.entrySet()) { + final var uuid = entry.getKey(); + final var serviceId = entry.getValue(); + insertStatement.setBytes(1, UuidUtil.toByteArray(uuid)); + insertStatement.setString(2, serviceId.toString()); + insertStatement.execute(); + } + } } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/Database.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/Database.java index 95f9e1fa0a787..6ca3847d4e0eb 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/Database.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/Database.java @@ -1,16 +1,16 @@ package org.asamk.signal.manager.storage; -import java.io.File; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.function.Function; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import org.slf4j.Logger; import org.sqlite.SQLiteConfig; import org.sqlite.SQLiteDataSource; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.function.Function; public abstract class Database implements AutoCloseable { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/SignalAccount.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/SignalAccount.java index afdab0b0f37ca..72725f5c251fb 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -76,6 +76,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.util.CredentialsProvider; @@ -101,6 +102,7 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; @@ -109,10 +111,10 @@ public class SignalAccount implements Closeable { - private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class); + private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class); private static final int MINIMUM_STORAGE_VERSION = 1; - private static final int CURRENT_STORAGE_VERSION = 8; + private static final int CURRENT_STORAGE_VERSION = 9; private final Object LOCK = new Object(); @@ -129,6 +131,7 @@ public class SignalAccount implements Closeable { private ServiceEnvironment serviceEnvironment; private String number; private String username; + private UsernameLinkComponents usernameLink; private String encryptedDeviceName; private int deviceId = 0; private String password; @@ -152,6 +155,10 @@ public class SignalAccount implements Closeable { private final KeyValueEntry storageManifestVersion = new KeyValueEntry<>("storage-manifest-version", long.class, -1L); + private final KeyValueEntry unrestrictedUnidentifiedAccess = new KeyValueEntry<>( + "unrestricted-unidentified-access", + Boolean.class, + false); private boolean isMultiDevice = false; private boolean registered = false; @@ -162,6 +169,7 @@ public class SignalAccount implements Closeable { private GroupStore groupStore; private RecipientStore recipientStore; private StickerStore stickerStore; + private UnknownStorageIdStore unknownStorageIdStore; private ConfigurationStore configurationStore; private KeyValueStore keyValueStore; private CdsiStore cdsiStore; @@ -170,6 +178,7 @@ public class SignalAccount implements Closeable { private MessageSendLogStore messageSendLogStore; private AccountDatabase accountDatabase; + private RecipientId selfRecipientId; private SignalAccount(final FileChannel fileChannel, final FileLock lock) { this.fileChannel = fileChannel; @@ -184,10 +193,9 @@ public static SignalAccount load( final var pair = openFileChannel(fileName, waitForLock); try { var signalAccount = new SignalAccount(pair.first(), pair.second()); - logger.trace("Loading account file"); signalAccount.load(dataPath, accountPath, settings); - logger.trace("Migrating legacy parts of account file"); signalAccount.migrateLegacyConfigs(); + signalAccount.init(); return signalAccount; } catch (Throwable e) { @@ -234,7 +242,7 @@ public static SignalAccount create( signalAccount.registered = false; signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION; - signalAccount.migrateLegacyConfigs(); + signalAccount.init(); signalAccount.save(); return signalAccount; @@ -280,6 +288,7 @@ public void setProvisioningData( this.number = number; this.aciAccountData.setServiceId(aci); this.pniAccountData.setServiceId(pni); + this.init(); getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress()); this.password = password; this.profileKey = profileKey; @@ -331,6 +340,7 @@ public void finishRegistration( this.registered = true; this.aciAccountData.setServiceId(aci); this.pniAccountData.setServiceId(pni); + init(); this.registrationLockPin = pin; getKeyValueStore().storeEntry(lastReceiveTimestamp, 0L); save(); @@ -350,8 +360,13 @@ public void initDatabase() { getAccountDatabase(); } + private void init() { + this.selfRecipientId = getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress()); + } + private void migrateLegacyConfigs() { if (isPrimaryDevice() && getPniIdentityKeyPair() == null) { + logger.trace("Migrating legacy parts of account file"); setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair()); } } @@ -370,8 +385,15 @@ public void removeRecipient(final RecipientId recipientId) { } getRecipientStore().deleteRecipientData(recipientId); getMessageCache().deleteMessages(recipientId); - if (recipientAddress.serviceId().isPresent()) { - final var serviceId = recipientAddress.serviceId().get(); + if (recipientAddress.aci().isPresent()) { + final var serviceId = recipientAddress.aci().get(); + aciAccountData.getSessionStore().deleteAllSessions(serviceId); + pniAccountData.getSessionStore().deleteAllSessions(serviceId); + getIdentityKeyStore().deleteIdentity(serviceId); + getSenderKeyStore().deleteAll(serviceId); + } + if (recipientAddress.pni().isPresent()) { + final var serviceId = recipientAddress.pni().get(); aciAccountData.getSessionStore().deleteAllSessions(serviceId); pniAccountData.getSessionStore().deleteAllSessions(serviceId); getIdentityKeyStore().deleteIdentity(serviceId); @@ -416,6 +438,7 @@ public static boolean accountFileExists(File dataPath, String account) { private void load( File dataPath, String accountPath, final Settings settings ) throws IOException { + logger.trace("Loading account file {}", accountPath); this.dataPath = dataPath; this.accountPath = accountPath; this.settings = settings; @@ -451,6 +474,9 @@ private void load( registered = storage.registered; number = storage.number; username = storage.username; + if ("".equals(username)) { + username = null; + } encryptedDeviceName = storage.encryptedDeviceName; deviceId = storage.deviceId; isMultiDevice = storage.isMultiDevice; @@ -474,7 +500,10 @@ private void load( e); } } - + if (storage.usernameLinkEntropy != null && storage.usernameLinkServerId != null) { + usernameLink = new UsernameLinkComponents(base64.decode(storage.usernameLinkEntropy), + UUID.fromString(storage.usernameLinkServerId)); + } } if (migratedLegacyConfig) { @@ -527,6 +556,9 @@ private void loadLegacyFile(final File userPath, final JsonNode rootNode) throws registered = Utils.getNotNullNode(rootNode, "registered").asBoolean(); if (rootNode.hasNonNull("usernameIdentifier")) { username = rootNode.get("usernameIdentifier").asText(); + if ("".equals(username)) { + username = null; + } } if (rootNode.hasNonNull("uuid")) { try { @@ -822,12 +854,17 @@ private void loadLegacyStores( final var recipientId = getRecipientStore().resolveRecipientTrusted(contact.getAddress()); getContactStore().storeContact(recipientId, new Contact(contact.name, + null, null, contact.color, contact.messageExpirationTime, + 0, + false, contact.blocked, contact.archived, - false)); + false, + false, + null)); // Store profile keys only in profile store var profileKeyString = contact.profileKey; @@ -931,7 +968,9 @@ private void save() { registrationLockPin, pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()), storageKey == null ? null : base64.encodeToString(storageKey.serialize()), - profileKey == null ? null : base64.encodeToString(profileKey.serialize())); + profileKey == null ? null : base64.encodeToString(profileKey.serialize()), + usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()), + usernameLink == null ? null : usernameLink.getServerId().toString()); try { try (var output = new ByteArrayOutputStream()) { // Write to memory first to prevent corrupting the file in case of serialization errors @@ -1072,7 +1111,7 @@ public void addKyberPreKeys(ServiceIdType serviceIdType, List serviceIdType, preKeyMetadata.nextKyberPreKeyId); accountData.getSignalServiceAccountDataStore() - .markAllOneTimeEcPreKeysStaleIfNecessary(System.currentTimeMillis()); + .markAllOneTimeKyberPreKeysStaleIfNecessary(System.currentTimeMillis()); for (var record : records) { if (preKeyMetadata.nextKyberPreKeyId != record.getId()) { logger.error("Invalid kyber pre key id {}, expected {}", @@ -1151,7 +1190,9 @@ public boolean isMultiDevice() { public IdentityKeyStore getIdentityKeyStore() { return getOrCreate(() -> identityKeyStore, - () -> identityKeyStore = new IdentityKeyStore(getAccountDatabase(), settings.trustNewIdentity())); + () -> identityKeyStore = new IdentityKeyStore(getAccountDatabase(), + settings.trustNewIdentity(), + getRecipientStore())); } public GroupStore getGroupStore() { @@ -1209,9 +1250,13 @@ private KeyValueStore getKeyValueStore() { return getOrCreate(() -> keyValueStore, () -> keyValueStore = new KeyValueStore(getAccountDatabase())); } + public UnknownStorageIdStore getUnknownStorageIdStore() { + return getOrCreate(() -> unknownStorageIdStore, () -> unknownStorageIdStore = new UnknownStorageIdStore()); + } + public ConfigurationStore getConfigurationStore() { return getOrCreate(() -> configurationStore, - () -> configurationStore = new ConfigurationStore(getKeyValueStore())); + () -> configurationStore = new ConfigurationStore(getKeyValueStore(), getRecipientStore())); } public MessageCache getMessageCache() { @@ -1282,6 +1327,14 @@ public void setUsername(final String username) { save(); } + public UsernameLinkComponents getUsernameLink() { + return usernameLink; + } + + public void setUsernameLink(final UsernameLinkComponents usernameLink) { + this.usernameLink = usernameLink; + } + public ServiceEnvironment getServiceEnvironment() { return serviceEnvironment; } @@ -1304,7 +1357,7 @@ public AccountAttributes getAccountAttributes(String registrationLock) { getAccountCapabilities(), encryptedDeviceName, pniAccountData.getLocalRegistrationId(), - null); // TODO recoveryPassword? + getRecoveryPassword()); } public AccountAttributes.Capabilities getAccountCapabilities() { @@ -1372,7 +1425,7 @@ public RecipientAddress getSelfRecipientAddress() { } public RecipientId getSelfRecipientId() { - return getRecipientResolver().resolveRecipient(getSelfRecipientAddress()); + return selfRecipientId; } public String getSessionId(final String forNumber) { @@ -1457,28 +1510,43 @@ public MasterKey getOrCreatePinMasterKey() { return pinMasterKey; } - public StorageKey getStorageKey() { - if (pinMasterKey != null) { - return pinMasterKey.deriveStorageServiceKey(); + public void setMasterKey(MasterKey masterKey) { + if (isPrimaryDevice()) { + return; } - return storageKey; + this.pinMasterKey = masterKey; + save(); } public StorageKey getOrCreateStorageKey() { - if (isPrimaryDevice()) { - return getOrCreatePinMasterKey().deriveStorageServiceKey(); + if (pinMasterKey != null) { + return pinMasterKey.deriveStorageServiceKey(); + } else if (storageKey != null) { + return storageKey; + } else if (!isPrimaryDevice() || !isMultiDevice()) { + // Only upload storage, if a pin master key already exists or linked devices exist + return null; } - return storageKey; + + return getOrCreatePinMasterKey().deriveStorageServiceKey(); } public void setStorageKey(final StorageKey storageKey) { - if (storageKey.equals(this.storageKey)) { + if (isPrimaryDevice() || storageKey.equals(this.storageKey)) { return; } this.storageKey = storageKey; save(); } + public String getRecoveryPassword() { + final var masterKey = getPinBackedMasterKey(); + if (masterKey == null) { + return null; + } + return masterKey.deriveRegistrationRecoveryPassword(); + } + public long getStorageManifestVersion() { return getKeyValueStore().getEntry(storageManifestVersion); } @@ -1583,8 +1651,11 @@ public void setLastReceiveTimestamp(final long value) { } public boolean isUnrestrictedUnidentifiedAccess() { - final var profile = getProfileStore().getProfile(getSelfRecipientId()); - return profile != null && profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED; + return Boolean.TRUE.equals(getKeyValueStore().getEntry(unrestrictedUnidentifiedAccess)); + } + + public void setUnrestrictedUnidentifiedAccess(boolean value) { + getKeyValueStore().storeEntry(unrestrictedUnidentifiedAccess, value); } public boolean isDiscoverableByPhoneNumber() { @@ -1788,7 +1859,9 @@ public record Storage( String registrationLockPin, String pinMasterKey, String storageKey, - String profileKey + String profileKey, + String usernameLinkEntropy, + String usernameLinkServerId ) { public record AccountData( diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/UnknownStorageIdStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/UnknownStorageIdStore.java new file mode 100644 index 0000000000000..7538975195108 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/UnknownStorageIdStore.java @@ -0,0 +1,105 @@ +package org.asamk.signal.manager.storage; + +import org.whispersystems.signalservice.api.storage.StorageId; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class UnknownStorageIdStore { + + private static final String TABLE_STORAGE_ID = "storage_id"; + + public static void createSql(Connection connection) throws SQLException { + // When modifying the CREATE statement here, also add a migration in AccountDatabase.java + try (final var statement = connection.createStatement()) { + statement.executeUpdate(""" + CREATE TABLE storage_id ( + _id INTEGER PRIMARY KEY, + type INTEGER NOT NULL, + storage_id BLOB UNIQUE NOT NULL + ) STRICT; + """); + } + } + + public Set getUnknownStorageIds(Connection connection) throws SQLException { + final var sql = ( + """ + SELECT s.type, s.storage_id + FROM %s s + """ + ).formatted(TABLE_STORAGE_ID); + try (final var statement = connection.prepareStatement(sql)) { + try (var result = Utils.executeQueryForStream(statement, this::getStorageIdFromResultSet)) { + return result.collect(Collectors.toSet()); + } + } + } + + public List getUnknownStorageIds( + Connection connection, Collection types + ) throws SQLException { + final var typesCommaSeparated = types.stream().map(String::valueOf).collect(Collectors.joining(",")); + final var sql = ( + """ + SELECT s.type, s.storage_id + FROM %s s + WHERE s.type IN (%s) + """ + ).formatted(TABLE_STORAGE_ID, typesCommaSeparated); + try (final var statement = connection.prepareStatement(sql)) { + try (var result = Utils.executeQueryForStream(statement, this::getStorageIdFromResultSet)) { + return result.toList(); + } + } + } + + public void addUnknownStorageIds(Connection connection, Collection storageIds) throws SQLException { + final var sql = ( + """ + INSERT OR REPLACE INTO %s (type, storage_id) + VALUES (?, ?) + """ + ).formatted(TABLE_STORAGE_ID); + try (final var statement = connection.prepareStatement(sql)) { + for (final var storageId : storageIds) { + statement.setInt(1, storageId.getType()); + statement.setBytes(2, storageId.getRaw()); + statement.executeUpdate(); + } + } + } + + public void deleteUnknownStorageIds(Connection connection, Collection storageIds) throws SQLException { + final var sql = ( + """ + DELETE FROM %s + WHERE storage_id = ? + """ + ).formatted(TABLE_STORAGE_ID); + try (final var statement = connection.prepareStatement(sql)) { + for (final var storageId : storageIds) { + statement.setBytes(1, storageId.getRaw()); + statement.executeUpdate(); + } + } + } + + public void deleteAllUnknownStorageIds(Connection connection) throws SQLException { + final var sql = "DELETE FROM %s".formatted(TABLE_STORAGE_ID); + try (final var statement = connection.prepareStatement(sql)) { + statement.executeUpdate(); + } + } + + private StorageId getStorageIdFromResultSet(ResultSet resultSet) throws SQLException { + final var type = resultSet.getInt("type"); + final var storageId = resultSet.getBytes("storage_id"); + return StorageId.forType(storageId, type); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java index 55c5adf0935e1..0237994105ef1 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/accounts/AccountsStore.java @@ -33,7 +33,7 @@ public class AccountsStore { private static final int MINIMUM_STORAGE_VERSION = 1; private static final int CURRENT_STORAGE_VERSION = 2; - private final static Logger logger = LoggerFactory.getLogger(AccountsStore.class); + private static final Logger logger = LoggerFactory.getLogger(AccountsStore.class); private final ObjectMapper objectMapper = Utils.createStorageObjectMapper(); private final File dataPath; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java index f85d3d8084f37..558d4f86d5218 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/configuration/ConfigurationStore.java @@ -3,10 +3,15 @@ import org.asamk.signal.manager.api.PhoneNumberSharingMode; import org.asamk.signal.manager.storage.keyValue.KeyValueEntry; import org.asamk.signal.manager.storage.keyValue.KeyValueStore; +import org.asamk.signal.manager.storage.recipients.RecipientStore; + +import java.sql.Connection; +import java.sql.SQLException; public class ConfigurationStore { private final KeyValueStore keyValueStore; + private final RecipientStore recipientStore; private final KeyValueEntry readReceipts = new KeyValueEntry<>("config-read-receipts", Boolean.class); private final KeyValueEntry unidentifiedDeliveryIndicators = new KeyValueEntry<>( @@ -20,9 +25,11 @@ public class ConfigurationStore { private final KeyValueEntry phoneNumberSharingMode = new KeyValueEntry<>( "config-phone-number-sharing-mode", PhoneNumberSharingMode.class); + private final KeyValueEntry usernameLinkColor = new KeyValueEntry<>("username-link-color", String.class); - public ConfigurationStore(final KeyValueStore keyValueStore) { + public ConfigurationStore(final KeyValueStore keyValueStore, RecipientStore recipientStore) { this.keyValueStore = keyValueStore; + this.recipientStore = recipientStore; } public Boolean getReadReceipts() { @@ -30,7 +37,15 @@ public Boolean getReadReceipts() { } public void setReadReceipts(final boolean value) { - keyValueStore.storeEntry(readReceipts, value); + if (keyValueStore.storeEntry(readReceipts, value)) { + recipientStore.rotateSelfStorageId(); + } + } + + public void setReadReceipts(final Connection connection, final boolean value) throws SQLException { + if (keyValueStore.storeEntry(connection, readReceipts, value)) { + recipientStore.rotateSelfStorageId(connection); + } } public Boolean getUnidentifiedDeliveryIndicators() { @@ -38,7 +53,17 @@ public Boolean getUnidentifiedDeliveryIndicators() { } public void setUnidentifiedDeliveryIndicators(final boolean value) { - keyValueStore.storeEntry(unidentifiedDeliveryIndicators, value); + if (keyValueStore.storeEntry(unidentifiedDeliveryIndicators, value)) { + recipientStore.rotateSelfStorageId(); + } + } + + public void setUnidentifiedDeliveryIndicators( + final Connection connection, final boolean value + ) throws SQLException { + if (keyValueStore.storeEntry(connection, unidentifiedDeliveryIndicators, value)) { + recipientStore.rotateSelfStorageId(connection); + } } public Boolean getTypingIndicators() { @@ -46,7 +71,15 @@ public Boolean getTypingIndicators() { } public void setTypingIndicators(final boolean value) { - keyValueStore.storeEntry(typingIndicators, value); + if (keyValueStore.storeEntry(typingIndicators, value)) { + recipientStore.rotateSelfStorageId(); + } + } + + public void setTypingIndicators(final Connection connection, final boolean value) throws SQLException { + if (keyValueStore.storeEntry(connection, typingIndicators, value)) { + recipientStore.rotateSelfStorageId(connection); + } } public Boolean getLinkPreviews() { @@ -54,7 +87,15 @@ public Boolean getLinkPreviews() { } public void setLinkPreviews(final boolean value) { - keyValueStore.storeEntry(linkPreviews, value); + if (keyValueStore.storeEntry(linkPreviews, value)) { + recipientStore.rotateSelfStorageId(); + } + } + + public void setLinkPreviews(final Connection connection, final boolean value) throws SQLException { + if (keyValueStore.storeEntry(connection, linkPreviews, value)) { + recipientStore.rotateSelfStorageId(connection); + } } public Boolean getPhoneNumberUnlisted() { @@ -62,7 +103,15 @@ public Boolean getPhoneNumberUnlisted() { } public void setPhoneNumberUnlisted(final boolean value) { - keyValueStore.storeEntry(phoneNumberUnlisted, value); + if (keyValueStore.storeEntry(phoneNumberUnlisted, value)) { + recipientStore.rotateSelfStorageId(); + } + } + + public void setPhoneNumberUnlisted(final Connection connection, final boolean value) throws SQLException { + if (keyValueStore.storeEntry(connection, phoneNumberUnlisted, value)) { + recipientStore.rotateSelfStorageId(connection); + } } public PhoneNumberSharingMode getPhoneNumberSharingMode() { @@ -70,6 +119,32 @@ public PhoneNumberSharingMode getPhoneNumberSharingMode() { } public void setPhoneNumberSharingMode(final PhoneNumberSharingMode value) { - keyValueStore.storeEntry(phoneNumberSharingMode, value); + if (keyValueStore.storeEntry(phoneNumberSharingMode, value)) { + recipientStore.rotateSelfStorageId(); + } + } + + public void setPhoneNumberSharingMode( + final Connection connection, final PhoneNumberSharingMode value + ) throws SQLException { + if (keyValueStore.storeEntry(connection, phoneNumberSharingMode, value)) { + recipientStore.rotateSelfStorageId(connection); + } + } + + public String getUsernameLinkColor() { + return keyValueStore.getEntry(usernameLinkColor); + } + + public void setUsernameLinkColor(final String color) { + if (keyValueStore.storeEntry(usernameLinkColor, color)) { + recipientStore.rotateSelfStorageId(); + } + } + + public void setUsernameLinkColor(final Connection connection, final String color) throws SQLException { + if (keyValueStore.storeEntry(connection, usernameLinkColor, color)) { + recipientStore.rotateSelfStorageId(connection); + } } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index a16af0ff34921..7de3a8f9bb537 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -46,6 +46,10 @@ public Set getAdminMembers() { public abstract void setBlocked(boolean blocked); + public abstract boolean isProfileSharingEnabled(); + + public abstract void setProfileSharingEnabled(boolean profileSharingEnabled); + public abstract int getMessageExpirationTimer(); public abstract boolean isAnnouncementGroup(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index ba09337cb7ddb..f377ff2d09744 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -25,6 +25,7 @@ public final class GroupInfoV1 extends GroupInfo { public int messageExpirationTime; public boolean blocked; public boolean archived; + private byte[] storageRecord; public GroupInfoV1(GroupIdV1 groupId) { this.groupId = groupId; @@ -38,7 +39,8 @@ public GroupInfoV1( final String color, final int messageExpirationTime, final boolean blocked, - final boolean archived + final boolean archived, + final byte[] storageRecord ) { this.groupId = groupId; this.expectedV2Id = expectedV2Id; @@ -48,6 +50,7 @@ public GroupInfoV1( this.messageExpirationTime = messageExpirationTime; this.blocked = blocked; this.archived = archived; + this.storageRecord = storageRecord; } @Override @@ -91,6 +94,15 @@ public void setBlocked(final boolean blocked) { this.blocked = blocked; } + @Override + public boolean isProfileSharingEnabled() { + return true; + } + + @Override + public void setProfileSharingEnabled(final boolean profileSharingEnabled) { + } + @Override public int getMessageExpirationTimer() { return messageExpirationTime; @@ -123,4 +135,8 @@ public void addMembers(Collection members) { public void removeMember(RecipientId recipientId) { this.members.removeIf(member -> member.equals(recipientId)); } + + public byte[] getStorageRecord() { + return storageRecord; + } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index c263aa9bcfff7..89306ecd18638 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -23,7 +23,9 @@ public final class GroupInfoV2 extends GroupInfo { private final GroupMasterKey masterKey; private final DistributionId distributionId; private boolean blocked; + private boolean profileSharingEnabled; private DecryptedGroup group; + private byte[] storageRecord; private boolean permissionDenied; private final RecipientResolver recipientResolver; @@ -43,7 +45,9 @@ public GroupInfoV2( final DecryptedGroup group, final DistributionId distributionId, final boolean blocked, + final boolean profileSharingEnabled, final boolean permissionDenied, + final byte[] storageRecord, final RecipientResolver recipientResolver ) { this.groupId = groupId; @@ -51,7 +55,9 @@ public GroupInfoV2( this.group = group; this.distributionId = distributionId; this.blocked = blocked; + this.profileSharingEnabled = profileSharingEnabled; this.permissionDenied = permissionDenied; + this.storageRecord = storageRecord; this.recipientResolver = recipientResolver; } @@ -64,6 +70,10 @@ public GroupMasterKey getMasterKey() { return masterKey; } + public byte[] getStorageRecord() { + return storageRecord; + } + public DistributionId getDistributionId() { return distributionId; } @@ -176,6 +186,16 @@ public void setBlocked(final boolean blocked) { this.blocked = blocked; } + @Override + public boolean isProfileSharingEnabled() { + return profileSharingEnabled; + } + + @Override + public void setProfileSharingEnabled(final boolean profileSharingEnabled) { + this.profileSharingEnabled = profileSharingEnabled; + } + @Override public int getMessageExpirationTimer() { return this.group != null && this.group.disappearingMessagesTimer != null diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupStore.java index 71fa0f9d4461c..1116309bd6651 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -9,12 +9,15 @@ import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientIdCreator; import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.util.KeyUtils; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; +import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.DistributionId; +import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -22,9 +25,11 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -32,7 +37,7 @@ public class GroupStore { - private final static Logger logger = LoggerFactory.getLogger(GroupStore.class); + private static final Logger logger = LoggerFactory.getLogger(GroupStore.class); private static final String TABLE_GROUP_V2 = "group_v2"; private static final String TABLE_GROUP_V1 = "group_v1"; private static final String TABLE_GROUP_V1_MEMBER = "group_v1_member"; @@ -47,15 +52,20 @@ public static void createSql(Connection connection) throws SQLException { statement.executeUpdate(""" CREATE TABLE group_v2 ( _id INTEGER PRIMARY KEY, + storage_id BLOB UNIQUE, + storage_record BLOB, group_id BLOB UNIQUE NOT NULL, master_key BLOB NOT NULL, group_data BLOB, distribution_id BLOB UNIQUE NOT NULL, blocked INTEGER NOT NULL DEFAULT FALSE, + profile_sharing INTEGER NOT NULL DEFAULT FALSE, permission_denied INTEGER NOT NULL DEFAULT FALSE ) STRICT; CREATE TABLE group_v1 ( _id INTEGER PRIMARY KEY, + storage_id BLOB UNIQUE, + storage_record BLOB, group_id BLOB UNIQUE NOT NULL, group_id_v2 BLOB UNIQUE, name TEXT, @@ -87,25 +97,63 @@ public GroupStore( public void updateGroup(GroupInfo group) { try (final var connection = database.getConnection()) { connection.setAutoCommit(false); - final Long internalId; - final var sql = ( - """ - SELECT g._id - FROM %s g - WHERE g.group_id = ? - """ - ).formatted(group instanceof GroupInfoV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2); - try (final var statement = connection.prepareStatement(sql)) { - statement.setBytes(1, group.getGroupId().serialize()); - internalId = Utils.executeQueryForOptional(statement, res -> res.getLong("_id")).orElse(null); - } - insertOrReplaceGroup(connection, internalId, group); + updateGroup(connection, group); connection.commit(); } catch (SQLException e) { throw new RuntimeException("Failed update recipient store", e); } } + public void updateGroup(final Connection connection, final GroupInfo group) throws SQLException { + final Long internalId; + final var sql = ( + """ + SELECT g._id + FROM %s g + WHERE g.group_id = ? + """ + ).formatted(group instanceof GroupInfoV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, group.getGroupId().serialize()); + internalId = Utils.executeQueryForOptional(statement, res -> res.getLong("_id")).orElse(null); + } + insertOrReplaceGroup(connection, internalId, group); + } + + public void storeStorageRecord( + final Connection connection, final GroupId groupId, final StorageId storageId, final byte[] storageRecord + ) throws SQLException { + final var groupTable = groupId instanceof GroupIdV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2; + final var deleteSql = ( + """ + UPDATE %s + SET storage_id = NULL + WHERE storage_id = ? + """ + ).formatted(groupTable); + try (final var statement = connection.prepareStatement(deleteSql)) { + statement.setBytes(1, storageId.getRaw()); + statement.executeUpdate(); + } + final var sql = ( + """ + UPDATE %s + SET storage_id = ?, storage_record = ? + WHERE group_id = ? + """ + ).formatted(groupTable); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, storageId.getRaw()); + if (storageRecord == null) { + statement.setNull(2, Types.BLOB); + } else { + statement.setBytes(2, storageRecord); + } + statement.setBytes(3, groupId.serialize()); + statement.executeUpdate(); + } + } + public void deleteGroup(GroupId groupId) { if (groupId instanceof GroupIdV1 groupIdV1) { deleteGroup(groupIdV1); @@ -115,30 +163,34 @@ public void deleteGroup(GroupId groupId) { } public void deleteGroup(GroupIdV1 groupIdV1) { - final var sql = ( - """ - DELETE FROM %s - WHERE group_id = ? - """ - ).formatted(TABLE_GROUP_V1); try (final var connection = database.getConnection()) { - try (final var statement = connection.prepareStatement(sql)) { - statement.setBytes(1, groupIdV1.serialize()); - statement.executeUpdate(); - } + deleteGroup(connection, groupIdV1); } catch (SQLException e) { throw new RuntimeException("Failed update group store", e); } } - public void deleteGroup(GroupIdV2 groupIdV2) { + private void deleteGroup(final Connection connection, final GroupIdV1 groupIdV1) throws SQLException { final var sql = ( """ DELETE FROM %s WHERE group_id = ? """ - ).formatted(TABLE_GROUP_V2); + ).formatted(TABLE_GROUP_V1); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, groupIdV1.serialize()); + statement.executeUpdate(); + } + } + + public void deleteGroup(GroupIdV2 groupIdV2) { try (final var connection = database.getConnection()) { + final var sql = ( + """ + DELETE FROM %s + WHERE group_id = ? + """ + ).formatted(TABLE_GROUP_V2); try (final var statement = connection.prepareStatement(sql)) { statement.setBytes(1, groupIdV2.serialize()); statement.executeUpdate(); @@ -150,23 +202,27 @@ public void deleteGroup(GroupIdV2 groupIdV2) { public GroupInfo getGroup(GroupId groupId) { try (final var connection = database.getConnection()) { - if (groupId instanceof GroupIdV1 groupIdV1) { - final var group = getGroup(connection, groupIdV1); - if (group != null) { - return group; - } - return getGroupV2ByV1Id(connection, groupIdV1); - } else if (groupId instanceof GroupIdV2 groupIdV2) { - final var group = getGroup(connection, groupIdV2); - if (group != null) { - return group; - } - return getGroupV1ByV2Id(connection, groupIdV2); - } + return getGroup(connection, groupId); } catch (SQLException e) { throw new RuntimeException("Failed read from group store", e); } - throw new AssertionError("Invalid group id type"); + } + + public GroupInfo getGroup(final Connection connection, final GroupId groupId) throws SQLException { + if (groupId instanceof GroupIdV1 groupIdV1) { + final var group = getGroup(connection, groupIdV1); + if (group != null) { + return group; + } + return getGroupV2ByV1Id(connection, groupIdV1); + } else { + GroupIdV2 groupIdV2 = (GroupIdV2) groupId; + final var group = getGroup(connection, groupIdV2); + if (group != null) { + return group; + } + return getGroupV1ByV2Id(connection, groupIdV2); + } } public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) { @@ -187,10 +243,79 @@ public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) { } } + public GroupInfoV2 getGroupOrPartialMigrate( + Connection connection, final GroupMasterKey groupMasterKey + ) throws SQLException { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + final var groupId = GroupUtils.getGroupIdV2(groupSecretParams); + + return getGroupOrPartialMigrate(connection, groupMasterKey, groupId); + } + + public GroupInfoV2 getGroupOrPartialMigrate( + final GroupMasterKey groupMasterKey, final GroupIdV2 groupId + ) { + try (final var connection = database.getConnection()) { + return getGroupOrPartialMigrate(connection, groupMasterKey, groupId); + } catch (SQLException e) { + throw new RuntimeException("Failed read from group store", e); + } + } + + private GroupInfoV2 getGroupOrPartialMigrate( + Connection connection, final GroupMasterKey groupMasterKey, final GroupIdV2 groupId + ) throws SQLException { + GroupInfo group = getGroup(groupId); + + if (group == null) { + return new GroupInfoV2(groupId, groupMasterKey, recipientResolver); + } + if (group instanceof GroupInfoV1 groupInfoV1) { + // Received a v2 group message for a v1 group, we need to locally migrate the group + deleteGroup(connection, groupInfoV1.getGroupId()); + final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver); + groupInfoV2.setBlocked(groupInfoV1.isBlocked()); + updateGroup(connection, groupInfoV2); + logger.debug("Locally migrated group {} to group v2, id: {}", groupInfoV1.getGroupId().toBase64(), + groupInfoV2.getGroupId().toBase64()); + return groupInfoV2; + } else { + return (GroupInfoV2) group; + } + } + public List getGroups() { return Stream.concat(getGroupsV2().stream(), getGroupsV1().stream()).toList(); } + public List getGroupV1Ids(Connection connection) throws SQLException { + final var sql = ( + """ + SELECT g.group_id + FROM %s g + """ + ).formatted(TABLE_GROUP_V1); + try (final var statement = connection.prepareStatement(sql)) { + return Utils.executeQueryForStream(statement, this::getGroupIdV1FromResultSet) + .filter(Objects::nonNull) + .toList(); + } + } + + public List getGroupV2Ids(Connection connection) throws SQLException { + final var sql = ( + """ + SELECT g.group_id + FROM %s g + """ + ).formatted(TABLE_GROUP_V2); + try (final var statement = connection.prepareStatement(sql)) { + return Utils.executeQueryForStream(statement, this::getGroupIdV2FromResultSet) + .filter(Objects::nonNull) + .toList(); + } + } + public void mergeRecipients( final Connection connection, final RecipientId recipientId, final RecipientId toBeMergedRecipientId ) throws SQLException { @@ -206,11 +331,111 @@ public void mergeRecipients( statement.setLong(2, toBeMergedRecipientId.id()); final var updatedRows = statement.executeUpdate(); if (updatedRows > 0) { - logger.info("Updated {} group members when merging recipients", updatedRows); + logger.debug("Updated {} group members when merging recipients", updatedRows); } } } + public List getStorageIds(Connection connection) throws SQLException { + final var storageIds = new ArrayList(); + final var sql = """ + SELECT g.storage_id + FROM %s g WHERE g.storage_id IS NOT NULL + """; + try (final var statement = connection.prepareStatement(sql.formatted(TABLE_GROUP_V1))) { + Utils.executeQueryForStream(statement, this::getGroupV1StorageIdFromResultSet).forEach(storageIds::add); + } + try (final var statement = connection.prepareStatement(sql.formatted(TABLE_GROUP_V2))) { + Utils.executeQueryForStream(statement, this::getGroupV2StorageIdFromResultSet).forEach(storageIds::add); + } + return storageIds; + } + + public void updateStorageIds( + Connection connection, Map storageIdV1Map, Map storageIdV2Map + ) throws SQLException { + final var sql = ( + """ + UPDATE %s + SET storage_id = ? + WHERE group_id = ? + """ + ); + try (final var statement = connection.prepareStatement(sql.formatted(TABLE_GROUP_V1))) { + for (final var entry : storageIdV1Map.entrySet()) { + statement.setBytes(1, entry.getValue().getRaw()); + statement.setBytes(2, entry.getKey().serialize()); + statement.executeUpdate(); + } + } + try (final var statement = connection.prepareStatement(sql.formatted(TABLE_GROUP_V2))) { + for (final var entry : storageIdV2Map.entrySet()) { + statement.setBytes(1, entry.getValue().getRaw()); + statement.setBytes(2, entry.getKey().serialize()); + statement.executeUpdate(); + } + } + } + + public void updateStorageId( + Connection connection, GroupId groupId, StorageId storageId + ) throws SQLException { + final var sqlV1 = ( + """ + UPDATE %s + SET storage_id = ? + WHERE group_id = ? + """ + ).formatted(groupId instanceof GroupIdV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2); + try (final var statement = connection.prepareStatement(sqlV1)) { + statement.setBytes(1, storageId.getRaw()); + statement.setBytes(2, groupId.serialize()); + statement.executeUpdate(); + } + } + + public void setMissingStorageIds() { + final var selectSql = ( + """ + SELECT g.group_id + FROM %s g + WHERE g.storage_id IS NULL + """ + ); + final var updateSql = ( + """ + UPDATE %s + SET storage_id = ? + WHERE group_id = ? + """ + ); + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); + try (final var selectStmt = connection.prepareStatement(selectSql.formatted(TABLE_GROUP_V1))) { + final var groupIds = Utils.executeQueryForStream(selectStmt, this::getGroupIdV1FromResultSet).toList(); + try (final var updateStmt = connection.prepareStatement(updateSql.formatted(TABLE_GROUP_V1))) { + for (final var groupId : groupIds) { + updateStmt.setBytes(1, KeyUtils.createRawStorageId()); + updateStmt.setBytes(2, groupId.serialize()); + } + } + } + try (final var selectStmt = connection.prepareStatement(selectSql.formatted(TABLE_GROUP_V2))) { + final var groupIds = Utils.executeQueryForStream(selectStmt, this::getGroupIdV2FromResultSet).toList(); + try (final var updateStmt = connection.prepareStatement(updateSql.formatted(TABLE_GROUP_V2))) { + for (final var groupId : groupIds) { + updateStmt.setBytes(1, KeyUtils.createRawStorageId()); + updateStmt.setBytes(2, groupId.serialize()); + updateStmt.executeUpdate(); + } + } + } + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed update group store", e); + } + } + void addLegacyGroups(final Collection groups) { logger.debug("Migrating legacy groups to database"); long start = System.nanoTime(); @@ -238,8 +463,8 @@ private void insertOrReplaceGroup( } } final var sql = """ - INSERT OR REPLACE INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT OR REPLACE INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING _id """.formatted(TABLE_GROUP_V1); try (final var statement = connection.prepareStatement(sql)) { @@ -255,6 +480,7 @@ private void insertOrReplaceGroup( statement.setLong(6, groupV1.getMessageExpirationTimer()); statement.setBoolean(7, groupV1.isBlocked()); statement.setBoolean(8, groupV1.archived); + statement.setBytes(9, KeyUtils.createRawStorageId()); final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper); if (internalId == null) { @@ -279,8 +505,8 @@ private void insertOrReplaceGroup( } else if (group instanceof GroupInfoV2 groupV2) { final var sql = ( """ - INSERT OR REPLACE INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, distribution_id) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT OR REPLACE INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """ ).formatted(TABLE_GROUP_V2); try (final var statement = connection.prepareStatement(sql)) { @@ -299,6 +525,8 @@ private void insertOrReplaceGroup( statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid())); statement.setBoolean(6, groupV2.isBlocked()); statement.setBoolean(7, groupV2.isPermissionDenied()); + statement.setBytes(8, KeyUtils.createRawStorageId()); + statement.setBoolean(9, groupV2.isProfileSharingEnabled()); statement.executeUpdate(); } } else { @@ -309,7 +537,7 @@ private void insertOrReplaceGroup( private List getGroupsV2() { final var sql = ( """ - SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied + SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record FROM %s g """ ).formatted(TABLE_GROUP_V2); @@ -324,20 +552,59 @@ private List getGroupsV2() { } } - private GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException { + public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException { + final var sql = ( + """ + SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record + FROM %s g + WHERE g.group_id = ? + """ + ).formatted(TABLE_GROUP_V2); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, groupIdV2.serialize()); + return Utils.executeQueryForOptional(statement, this::getGroupInfoV2FromResultSet).orElse(null); + } + } + + public StorageId getGroupStorageId(Connection connection, GroupIdV2 groupIdV2) throws SQLException { final var sql = ( """ - SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.permission_denied + SELECT g.storage_id FROM %s g WHERE g.group_id = ? """ ).formatted(TABLE_GROUP_V2); try (final var statement = connection.prepareStatement(sql)) { statement.setBytes(1, groupIdV2.serialize()); + final var storageId = Utils.executeQueryForOptional(statement, this::getGroupV2StorageIdFromResultSet); + if (storageId.isPresent()) { + return storageId.get(); + } + } + final var newStorageId = StorageId.forGroupV2(KeyUtils.createRawStorageId()); + updateStorageId(connection, groupIdV2, newStorageId); + return newStorageId; + } + + public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException { + final var sql = ( + """ + SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record + FROM %s g + WHERE g.storage_id = ? + """ + ).formatted(TABLE_GROUP_V2); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, storageId.getRaw()); return Utils.executeQueryForOptional(statement, this::getGroupInfoV2FromResultSet).orElse(null); } } + private GroupIdV2 getGroupIdV2FromResultSet(ResultSet resultSet) throws SQLException { + final var groupId = resultSet.getBytes("group_id"); + return GroupId.v2(groupId); + } + private GroupInfoV2 getGroupInfoV2FromResultSet(ResultSet resultSet) throws SQLException { try { final var groupId = resultSet.getBytes("group_id"); @@ -345,23 +612,41 @@ private GroupInfoV2 getGroupInfoV2FromResultSet(ResultSet resultSet) throws SQLE final var groupData = resultSet.getBytes("group_data"); final var distributionId = resultSet.getBytes("distribution_id"); final var blocked = resultSet.getBoolean("blocked"); + final var profileSharingEnabled = resultSet.getBoolean("profile_sharing"); final var permissionDenied = resultSet.getBoolean("permission_denied"); + final var storageRecord = resultSet.getBytes("storage_record"); return new GroupInfoV2(GroupId.v2(groupId), new GroupMasterKey(masterKey), groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData), DistributionId.from(UuidUtil.parseOrThrow(distributionId)), blocked, + profileSharingEnabled, permissionDenied, + storageRecord, recipientResolver); } catch (InvalidInputException | IOException e) { return null; } } + private StorageId getGroupV1StorageIdFromResultSet(ResultSet resultSet) throws SQLException { + final var storageId = resultSet.getBytes("storage_id"); + return storageId == null + ? StorageId.forGroupV1(KeyUtils.createRawStorageId()) + : StorageId.forGroupV1(storageId); + } + + private StorageId getGroupV2StorageIdFromResultSet(ResultSet resultSet) throws SQLException { + final var storageId = resultSet.getBytes("storage_id"); + return storageId == null + ? StorageId.forGroupV2(KeyUtils.createRawStorageId()) + : StorageId.forGroupV2(storageId); + } + private List getGroupsV1() { final var sql = ( """ - SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived + SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record FROM %s g """ ).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1); @@ -376,10 +661,10 @@ private List getGroupsV1() { } } - private GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException { + public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException { final var sql = ( """ - SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived + SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record FROM %s g WHERE g.group_id = ? """ @@ -390,6 +675,45 @@ private GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws } } + public StorageId getGroupStorageId(Connection connection, GroupIdV1 groupIdV1) throws SQLException { + final var sql = ( + """ + SELECT g.storage_id + FROM %s g + WHERE g.group_id = ? + """ + ).formatted(TABLE_GROUP_V1); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, groupIdV1.serialize()); + final var storageId = Utils.executeQueryForOptional(statement, this::getGroupV1StorageIdFromResultSet); + if (storageId.isPresent()) { + return storageId.get(); + } + } + final var newStorageId = StorageId.forGroupV1(KeyUtils.createRawStorageId()); + updateStorageId(connection, groupIdV1, newStorageId); + return newStorageId; + } + + public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException { + final var sql = ( + """ + SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record + FROM %s g + WHERE g.storage_id = ? + """ + ).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, storageId.getRaw()); + return Utils.executeQueryForOptional(statement, this::getGroupInfoV1FromResultSet).orElse(null); + } + } + + private GroupIdV1 getGroupIdV1FromResultSet(ResultSet resultSet) throws SQLException { + final var groupId = resultSet.getBytes("group_id"); + return GroupId.v1(groupId); + } + private GroupInfoV1 getGroupInfoV1FromResultSet(ResultSet resultSet) throws SQLException { final var groupId = resultSet.getBytes("group_id"); final var groupIdV2 = resultSet.getBytes("group_id_v2"); @@ -405,6 +729,7 @@ private GroupInfoV1 getGroupInfoV1FromResultSet(ResultSet resultSet) throws SQLE final var expirationTime = resultSet.getInt("expiration_time"); final var blocked = resultSet.getBoolean("blocked"); final var archived = resultSet.getBoolean("archived"); + final var storagRecord = resultSet.getBytes("storage_record"); return new GroupInfoV1(GroupId.v1(groupId), groupIdV2 == null ? null : GroupId.v2(groupIdV2), name, @@ -412,7 +737,8 @@ private GroupInfoV1 getGroupInfoV1FromResultSet(ResultSet resultSet) throws SQLE color, expirationTime, blocked, - archived); + archived, + storagRecord); } private GroupInfoV2 getGroupV2ByV1Id(final Connection connection, final GroupIdV1 groupId) throws SQLException { @@ -422,7 +748,7 @@ private GroupInfoV2 getGroupV2ByV1Id(final Connection connection, final GroupIdV private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException { final var sql = ( """ - SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived + SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record FROM %s g WHERE g.group_id_v2 = ? """ diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java index b53b14c0ae167..1d8c8799bc574 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/groups/LegacyGroupStore.java @@ -33,7 +33,7 @@ public class LegacyGroupStore { - private final static Logger logger = LoggerFactory.getLogger(LegacyGroupStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacyGroupStore.class); public static void migrate( final Storage storage, @@ -45,7 +45,7 @@ public static void migrate( if (g instanceof Storage.GroupV1 g1) { final var members = g1.members.stream().map(m -> { if (m.recipientId == null) { - return recipientResolver.resolveRecipient(new RecipientAddress(ServiceId.parseOrNull(m.uuid), + return recipientResolver.resolveRecipient(new RecipientAddress(ServiceId.ACI.parseOrNull(m.uuid), m.number)); } @@ -59,7 +59,8 @@ public static void migrate( g1.color, g1.messageExpirationTime, g1.blocked, - g1.archived); + g1.archived, + null); } final var g2 = (Storage.GroupV2) g; @@ -76,7 +77,9 @@ public static void migrate( loadDecryptedGroupLocked(groupId, groupCachePath), g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId), g2.blocked, + true, g2.permissionDenied, + null, recipientResolver); }).toList(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index 3195df171a8ca..a4b355ea66c2c 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.TrustNewIdentity; import org.asamk.signal.manager.storage.Database; import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.recipients.RecipientStore; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction; @@ -23,10 +24,11 @@ public class IdentityKeyStore { - private final static Logger logger = LoggerFactory.getLogger(IdentityKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(IdentityKeyStore.class); private static final String TABLE_IDENTITY = "identity"; private final Database database; private final TrustNewIdentity trustNewIdentity; + private final RecipientStore recipientStore; private final PublishSubject identityChanges = PublishSubject.create(); private boolean isRetryingDecryption = false; @@ -46,9 +48,12 @@ CREATE TABLE identity ( } } - public IdentityKeyStore(final Database database, final TrustNewIdentity trustNewIdentity) { + public IdentityKeyStore( + final Database database, final TrustNewIdentity trustNewIdentity, RecipientStore recipientStore + ) { this.database = database; this.trustNewIdentity = trustNewIdentity; + this.recipientStore = recipientStore; } public Observable getIdentityChanges() { @@ -59,58 +64,79 @@ public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identit return saveIdentity(serviceId.toString(), identityKey); } + public boolean saveIdentity( + final Connection connection, final ServiceId serviceId, final IdentityKey identityKey + ) throws SQLException { + return saveIdentity(connection, serviceId.toString(), identityKey); + } + boolean saveIdentity(final String address, final IdentityKey identityKey) { if (isRetryingDecryption) { return false; } try (final var connection = database.getConnection()) { - final var identityInfo = loadIdentity(connection, address); - if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) { - // Identity already exists, not updating the trust level - logger.trace("Not storing new identity for recipient {}, identity already stored", address); - return false; - } - - saveNewIdentity(connection, address, identityKey, identityInfo == null); - return true; + return saveIdentity(connection, address, identityKey); } catch (SQLException e) { throw new RuntimeException("Failed update identity store", e); } } + private boolean saveIdentity( + final Connection connection, final String address, final IdentityKey identityKey + ) throws SQLException { + final var identityInfo = loadIdentity(connection, address); + if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) { + // Identity already exists, not updating the trust level + logger.trace("Not storing new identity for recipient {}, identity already stored", address); + return false; + } + + saveNewIdentity(connection, address, identityKey, identityInfo == null); + return true; + } + public void setRetryingDecryption(final boolean retryingDecryption) { isRetryingDecryption = retryingDecryption; } public boolean setIdentityTrustLevel(ServiceId serviceId, IdentityKey identityKey, TrustLevel trustLevel) { try (final var connection = database.getConnection()) { - final var address = serviceId.toString(); - final var identityInfo = loadIdentity(connection, address); - if (identityInfo == null) { - logger.debug("Not updating trust level for recipient {}, identity not found", serviceId); - return false; - } - if (!identityInfo.getIdentityKey().equals(identityKey)) { - logger.debug("Not updating trust level for recipient {}, different identity found", serviceId); - return false; - } - if (identityInfo.getTrustLevel() == trustLevel) { - logger.trace("Not updating trust level for recipient {}, trust level already matches", serviceId); - return false; - } - - logger.debug("Updating trust level for recipient {} with trust {}", serviceId, trustLevel); - final var newIdentityInfo = new IdentityInfo(address, - identityKey, - trustLevel, - identityInfo.getDateAddedTimestamp()); - storeIdentity(connection, newIdentityInfo); - return true; + return setIdentityTrustLevel(connection, serviceId, identityKey, trustLevel); } catch (SQLException e) { throw new RuntimeException("Failed update identity store", e); } } + public boolean setIdentityTrustLevel( + final Connection connection, + final ServiceId serviceId, + final IdentityKey identityKey, + final TrustLevel trustLevel + ) throws SQLException { + final var address = serviceId.toString(); + final var identityInfo = loadIdentity(connection, address); + if (identityInfo == null) { + logger.debug("Not updating trust level for recipient {}, identity not found", serviceId); + return false; + } + if (!identityInfo.getIdentityKey().equals(identityKey)) { + logger.debug("Not updating trust level for recipient {}, different identity found", serviceId); + return false; + } + if (identityInfo.getTrustLevel() == trustLevel) { + logger.trace("Not updating trust level for recipient {}, trust level already matches", serviceId); + return false; + } + + logger.debug("Updating trust level for recipient {} with trust {}", serviceId, trustLevel); + final var newIdentityInfo = new IdentityInfo(address, + identityKey, + trustLevel, + identityInfo.getDateAddedTimestamp()); + storeIdentity(connection, newIdentityInfo); + return true; + } + public boolean isTrustedIdentity(ServiceId serviceId, IdentityKey identityKey, Direction direction) { return isTrustedIdentity(serviceId.toString(), identityKey, direction); } @@ -159,6 +185,10 @@ public IdentityInfo getIdentityInfo(String address) { } } + public IdentityInfo getIdentityInfo(Connection connection, String address) throws SQLException { + return loadIdentity(connection, address); + } + public List getIdentities() { try (final var connection = database.getConnection()) { final var sql = ( @@ -252,6 +282,7 @@ private void storeIdentity(final Connection connection, final IdentityInfo ident statement.setInt(4, identityInfo.getTrustLevel().ordinal()); statement.executeUpdate(); } + recipientStore.rotateStorageId(connection, identityInfo.getServiceId()); } private void deleteIdentity(final Connection connection, final String address) throws SQLException { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java index 27cb6f84005de..a7f6a4aa7e414 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java @@ -24,7 +24,7 @@ public class LegacyIdentityKeyStore { - private final static Logger logger = LoggerFactory.getLogger(LegacyIdentityKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacyIdentityKeyStore.class); private static final ObjectMapper objectMapper = org.asamk.signal.manager.storage.Utils.createStorageObjectMapper(); public static void migrate( diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java index 9b239b727e103..c51f32f1e863f 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java @@ -10,11 +10,12 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.util.Objects; public class KeyValueStore { private static final String TABLE_KEY_VALUE = "key_value"; - private final static Logger logger = LoggerFactory.getLogger(KeyValueStore.class); + private static final Logger logger = LoggerFactory.getLogger(KeyValueStore.class); private final Database database; @@ -36,6 +37,22 @@ public KeyValueStore(final Database database) { } public T getEntry(KeyValueEntry key) { + try (final var connection = database.getConnection()) { + return getEntry(connection, key); + } catch (SQLException e) { + throw new RuntimeException("Failed read from pre_key store", e); + } + } + + public boolean storeEntry(KeyValueEntry key, T value) { + try (final var connection = database.getConnection()) { + return storeEntry(connection, key, value); + } catch (SQLException e) { + throw new RuntimeException("Failed update key_value store", e); + } + } + + private T getEntry(final Connection connection, final KeyValueEntry key) throws SQLException { final var sql = ( """ SELECT key, value @@ -43,24 +60,27 @@ public T getEntry(KeyValueEntry key) { WHERE p.key = ? """ ).formatted(TABLE_KEY_VALUE); - try (final var connection = database.getConnection()) { - try (final var statement = connection.prepareStatement(sql)) { - statement.setString(1, key.key()); + try (final var statement = connection.prepareStatement(sql)) { + statement.setString(1, key.key()); - final var result = Utils.executeQueryForOptional(statement, - resultSet -> readValueFromResultSet(key, resultSet)).orElse(null); + final var result = Utils.executeQueryForOptional(statement, + resultSet -> readValueFromResultSet(key, resultSet)).orElse(null); - if (result == null) { - return key.defaultValue(); - } - return result; + if (result == null) { + return key.defaultValue(); } - } catch (SQLException e) { - throw new RuntimeException("Failed read from pre_key store", e); + return result; } } - public void storeEntry(KeyValueEntry key, T value) { + public boolean storeEntry( + final Connection connection, final KeyValueEntry key, final T value + ) throws SQLException { + final var entry = getEntry(key); + if (Objects.equals(entry, value)) { + return false; + } + final var sql = ( """ INSERT INTO %s (key, value) @@ -68,15 +88,12 @@ public void storeEntry(KeyValueEntry key, T value) { ON CONFLICT (key) DO UPDATE SET value=excluded.value """ ).formatted(TABLE_KEY_VALUE); - try (final var connection = database.getConnection()) { - try (final var statement = connection.prepareStatement(sql)) { - statement.setString(1, key.key()); - setParameterValue(statement, 2, key.clazz(), value); - statement.executeUpdate(); - } - } catch (SQLException e) { - throw new RuntimeException("Failed update key_value store", e); + try (final var statement = connection.prepareStatement(sql)) { + statement.setString(1, key.key()); + setParameterValue(statement, 2, key.clazz(), value); + statement.executeUpdate(); } + return true; } @SuppressWarnings("unchecked") diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java index e053b91fb4e23..4b2cea9b02425 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java @@ -11,7 +11,7 @@ public final class CachedMessage { - private final static Logger logger = LoggerFactory.getLogger(CachedMessage.class); + private static final Logger logger = LoggerFactory.getLogger(CachedMessage.class); private final File file; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java index 30452fbb8cdbd..51882a442835f 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java @@ -17,7 +17,7 @@ public class MessageCache { - private final static Logger logger = LoggerFactory.getLogger(MessageCache.class); + private static final Logger logger = LoggerFactory.getLogger(MessageCache.class); private final File messageCachePath; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java index 4067e2fe4e546..7b05dff96fc1c 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/KyberPreKeyStore.java @@ -20,7 +20,7 @@ public class KyberPreKeyStore implements SignalServiceKyberPreKeyStore { private static final String TABLE_KYBER_PRE_KEY = "kyber_pre_key"; - private final static Logger logger = LoggerFactory.getLogger(KyberPreKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(KyberPreKeyStore.class); private final Database database; private final int accountIdType; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacyPreKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacyPreKeyStore.java index f1151b2328b8a..c2c236dfe05f2 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacyPreKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacyPreKeyStore.java @@ -14,7 +14,7 @@ public class LegacyPreKeyStore { - private final static Logger logger = LoggerFactory.getLogger(LegacyPreKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacyPreKeyStore.class); static final Pattern preKeyFileNamePattern = Pattern.compile("(\\d+)"); public static void migrate(File preKeysPath, PreKeyStore preKeyStore) { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacySignedPreKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacySignedPreKeyStore.java index 8e34776c45a6f..a7133223225fd 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacySignedPreKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/LegacySignedPreKeyStore.java @@ -14,7 +14,7 @@ public class LegacySignedPreKeyStore { - private final static Logger logger = LoggerFactory.getLogger(LegacySignedPreKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacySignedPreKeyStore.class); static final Pattern signedPreKeyFileNamePattern = Pattern.compile("(\\d+)"); public static void migrate(File signedPreKeysPath, SignedPreKeyStore signedPreKeyStore) { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java index c3ac9632cfefc..d2d557108172d 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java @@ -20,7 +20,7 @@ public class PreKeyStore implements SignalServicePreKeyStore { private static final String TABLE_PRE_KEY = "pre_key"; - private final static Logger logger = LoggerFactory.getLogger(PreKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(PreKeyStore.class); private final Database database; private final int accountIdType; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java index 7c726f1693039..0de10b136d37a 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java @@ -23,7 +23,7 @@ public class SignedPreKeyStore implements org.signal.libsignal.protocol.state.SignedPreKeyStore { private static final String TABLE_SIGNED_PRE_KEY = "signed_pre_key"; - private final static Logger logger = LoggerFactory.getLogger(SignedPreKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(SignedPreKeyStore.class); private final Database database; private final int accountIdType; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java index cf1002576a6ce..abb66470dad3e 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java @@ -43,7 +43,9 @@ public List deserialize( if (node.isArray()) { for (var entry : node) { var name = entry.hasNonNull("name") ? entry.get("name").asText() : null; - var serviceId = entry.hasNonNull("uuid") ? ServiceId.parseOrNull(entry.get("uuid").asText()) : null; + var serviceId = entry.hasNonNull("uuid") + ? ServiceId.ACI.parseOrNull(entry.get("uuid").asText()) + : null; final var address = new RecipientAddress(serviceId, name); ProfileKey profileKey = null; try { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java index 1e95eaff99620..0d12a303c0d3b 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java @@ -25,7 +25,7 @@ public class LegacyJsonIdentityKeyStore { - private final static Logger logger = LoggerFactory.getLogger(LegacyJsonIdentityKeyStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacyJsonIdentityKeyStore.class); private final List identities; private final IdentityKeyPair identityKeyPair; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java index 039e8471a5d10..fb1e188b76962 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java @@ -36,7 +36,7 @@ public List deserialize( if (node.isArray()) { for (var recipient : node) { var recipientName = recipient.get("name").asText(); - var serviceId = ServiceId.parseOrThrow(recipient.get("uuid").asText()); + var serviceId = ServiceId.ACI.parseOrThrow(recipient.get("uuid").asText()); addresses.add(new RecipientAddress(serviceId, recipientName)); } } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java index f1b771808f742..17f136ec90fda 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java @@ -24,7 +24,7 @@ public class LegacyRecipientStore2 { - private final static Logger logger = LoggerFactory.getLogger(LegacyRecipientStore2.class); + private static final Logger logger = LoggerFactory.getLogger(LegacyRecipientStore2.class); public static void migrate(File file, RecipientStore recipientStore) { final var objectMapper = Utils.createStorageObjectMapper(); @@ -39,12 +39,17 @@ public static void migrate(File file, RecipientStore recipientStore) { Contact contact = null; if (r.contact != null) { contact = new Contact(r.contact.name, + null, null, r.contact.color, r.contact.messageExpirationTime, + 0, + false, r.contact.blocked, r.contact.archived, - r.contact.profileSharingEnabled); + r.contact.profileSharingEnabled, + false, + null); } ProfileKey profileKey = null; @@ -82,7 +87,13 @@ public static void migrate(File file, RecipientStore recipientStore) { .collect(Collectors.toSet())); } - return new Recipient(recipientId, address, contact, profileKey, expiringProfileKeyCredential, profile); + return new Recipient(recipientId, + address, + contact, + profileKey, + expiringProfileKeyCredential, + profile, + null); }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r)); recipientStore.addLegacyRecipients(recipients); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java index d9fecaa524be4..2fa91a5994d28 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/MergeRecipientHelper.java @@ -12,7 +12,7 @@ public class MergeRecipientHelper { - private final static Logger logger = LoggerFactory.getLogger(MergeRecipientHelper.class); + private static final Logger logger = LoggerFactory.getLogger(MergeRecipientHelper.class); static Pair> resolveRecipientTrustedLocked( Store store, RecipientAddress address @@ -32,22 +32,19 @@ static Pair> resolveRecipientTrustedLocked( return new Pair<>(recipient.id(), List.of()); } - if (recipient.address().serviceId().isEmpty() || ( - recipient.address().serviceId().equals(address.serviceId()) - ) || ( - recipient.address().pni().isPresent() && recipient.address().pni().equals(address.serviceId()) - ) || ( - recipient.address().serviceId().equals(address.pni()) - ) || ( - address.pni().isPresent() && address.pni().equals(recipient.address().pni()) - )) { + if (recipient.address().aci().isEmpty() || ( + address.aci().isEmpty() && ( + address.pni().isEmpty() + || recipient.address().pni().equals(address.pni()) + ) + ) || recipient.address().aci().equals(address.aci())) { logger.debug("Got existing recipient {}, updating with high trust address", recipient.id()); store.updateRecipientAddress(recipient.id(), recipient.address().withIdentifiersFrom(address)); return new Pair<>(recipient.id(), List.of()); } logger.debug( - "Got recipient {} existing with number/pni/username, but different serviceId, so stripping its number and adding new recipient", + "Got recipient {} existing with number/pni/username, but different aci, so stripping its number and adding new recipient", recipient.id()); store.updateRecipientAddress(recipient.id(), recipient.address().removeIdentifiersFrom(address)); @@ -55,14 +52,10 @@ static Pair> resolveRecipientTrustedLocked( } var resultingRecipient = recipients.stream() - .filter(r -> r.address().serviceId().equals(address.serviceId()) || r.address() - .pni() - .equals(address.serviceId())) + .filter(r -> r.address().aci().isPresent() && r.address().aci().equals(address.aci())) .findFirst(); if (resultingRecipient.isEmpty() && address.pni().isPresent()) { - resultingRecipient = recipients.stream().filter(r -> r.address().serviceId().equals(address.pni()) || ( - address.serviceId().equals(address.pni()) && r.address().pni().equals(address.pni()) - )).findFirst(); + resultingRecipient = recipients.stream().filter(r -> r.address().pni().equals(address.pni())).findFirst(); } final Set remainingRecipients; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/Recipient.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/Recipient.java index 3790ecde98e8b..1d5fb9c81094b 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/Recipient.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/Recipient.java @@ -21,13 +21,16 @@ public class Recipient { private final Profile profile; + private final byte[] storageRecord; + public Recipient( final RecipientId recipientId, final RecipientAddress address, final Contact contact, final ProfileKey profileKey, final ExpiringProfileKeyCredential expiringProfileKeyCredential, - final Profile profile + final Profile profile, + final byte[] storageRecord ) { this.recipientId = recipientId; this.address = address; @@ -35,6 +38,7 @@ public Recipient( this.profileKey = profileKey; this.expiringProfileKeyCredential = expiringProfileKeyCredential; this.profile = profile; + this.storageRecord = storageRecord; } private Recipient(final Builder builder) { @@ -42,8 +46,9 @@ private Recipient(final Builder builder) { address = builder.address; contact = builder.contact; profileKey = builder.profileKey; - expiringProfileKeyCredential = builder.expiringProfileKeyCredential1; + expiringProfileKeyCredential = builder.expiringProfileKeyCredential; profile = builder.profile; + storageRecord = builder.storageRecord; } public static Builder newBuilder() { @@ -56,8 +61,9 @@ public static Builder newBuilder(final Recipient copy) { builder.address = copy.getAddress(); builder.contact = copy.getContact(); builder.profileKey = copy.getProfileKey(); - builder.expiringProfileKeyCredential1 = copy.getExpiringProfileKeyCredential(); + builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential(); builder.profile = copy.getProfile(); + builder.storageRecord = copy.getStorageRecord(); return builder; } @@ -85,6 +91,10 @@ public Profile getProfile() { return profile; } + public byte[] getStorageRecord() { + return storageRecord; + } + @Override public boolean equals(final Object o) { if (this == o) return true; @@ -109,8 +119,9 @@ public static final class Builder { private RecipientAddress address; private Contact contact; private ProfileKey profileKey; - private ExpiringProfileKeyCredential expiringProfileKeyCredential1; + private ExpiringProfileKeyCredential expiringProfileKeyCredential; private Profile profile; + private byte[] storageRecord; private Builder() { } @@ -136,7 +147,7 @@ public Builder withProfileKey(final ProfileKey val) { } public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) { - expiringProfileKeyCredential1 = val; + expiringProfileKeyCredential = val; return this; } @@ -145,6 +156,11 @@ public Builder withProfile(final Profile val) { return this; } + public Builder withStorageRecord(final byte[] val) { + storageRecord = val; + return this; + } + public Recipient build() { return new Recipient(this); } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index 86ada86ab38d6..7eacb6a42f549 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -8,59 +8,64 @@ import java.util.Optional; public record RecipientAddress( - Optional serviceId, Optional pni, Optional number, Optional username + Optional aci, Optional pni, Optional number, Optional username ) { /** * Construct a RecipientAddress. * - * @param serviceId The ACI or PNI of the user, if available. - * @param number The phone number of the user, if available. + * @param aci The ACI of the user, if available. + * @param pni The PNI of the user, if available. + * @param number The phone number of the user, if available. + * @param username The username of the user, if available. */ public RecipientAddress { - if (serviceId.isPresent() && serviceId.get().isUnknown()) { - serviceId = Optional.empty(); + if (aci.isPresent() && aci.get().isUnknown()) { + aci = Optional.empty(); } if (pni.isPresent() && pni.get().isUnknown()) { pni = Optional.empty(); } - if (serviceId.isEmpty() && pni.isPresent()) { - serviceId = Optional.of(pni.get()); - } - if (serviceId.isPresent() && serviceId.get() instanceof PNI sPNI) { - if (pni.isPresent() && !sPNI.equals(pni.get())) { - throw new AssertionError("Must not have two different PNIs!"); - } - if (pni.isEmpty()) { - pni = Optional.of(sPNI); - } - } - if (serviceId.isEmpty() && number.isEmpty()) { - throw new AssertionError("Must have either a ServiceId or E164 number!"); + if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) { + throw new AssertionError("Must have either a ServiceId, username or E164 number!"); } } public RecipientAddress(Optional serviceId, Optional number) { - this(serviceId, Optional.empty(), number, Optional.empty()); + this(serviceId.filter(s -> s instanceof ACI).map(s -> (ACI) s), + serviceId.filter(s -> s instanceof PNI).map(s -> (PNI) s), + number, + Optional.empty()); + } + + public RecipientAddress(ACI aci, String e164) { + this(Optional.ofNullable(aci), Optional.empty(), Optional.ofNullable(e164), Optional.empty()); + } + + public RecipientAddress(PNI pni, String e164) { + this(Optional.empty(), Optional.ofNullable(pni), Optional.ofNullable(e164), Optional.empty()); } - public RecipientAddress(ServiceId serviceId, String e164) { - this(Optional.ofNullable(serviceId), Optional.empty(), Optional.ofNullable(e164), Optional.empty()); + public RecipientAddress(String e164) { + this(Optional.empty(), Optional.empty(), Optional.ofNullable(e164), Optional.empty()); } - public RecipientAddress(ServiceId serviceId, PNI pni, String e164) { - this(Optional.ofNullable(serviceId), Optional.ofNullable(pni), Optional.ofNullable(e164), Optional.empty()); + public RecipientAddress(ACI aci, PNI pni, String e164) { + this(Optional.ofNullable(aci), Optional.ofNullable(pni), Optional.ofNullable(e164), Optional.empty()); } - public RecipientAddress(ServiceId serviceId, PNI pni, String e164, String username) { - this(Optional.ofNullable(serviceId), + public RecipientAddress(ACI aci, PNI pni, String e164, String username) { + this(Optional.ofNullable(aci), Optional.ofNullable(pni), Optional.ofNullable(e164), Optional.ofNullable(username)); } public RecipientAddress(SignalServiceAddress address) { - this(Optional.of(address.getServiceId()), Optional.empty(), address.getNumber(), Optional.empty()); + this(address.getServiceId() instanceof ACI ? Optional.of((ACI) address.getServiceId()) : Optional.empty(), + address.getServiceId() instanceof PNI ? Optional.of((PNI) address.getServiceId()) : Optional.empty(), + address.getNumber(), + Optional.empty()); } public RecipientAddress(org.asamk.signal.manager.api.RecipientAddress address) { @@ -72,26 +77,28 @@ public RecipientAddress(ServiceId serviceId) { } public RecipientAddress withIdentifiersFrom(RecipientAddress address) { - return new RecipientAddress(( - this.serviceId.isEmpty() || this.isServiceIdPNI() || this.serviceId.equals(address.pni) - ) && !address.isServiceIdPNI() ? address.serviceId : this.serviceId, + return new RecipientAddress(address.aci.or(this::aci), address.pni.or(this::pni), address.number.or(this::number), address.username.or(this::username)); } public RecipientAddress removeIdentifiersFrom(RecipientAddress address) { - return new RecipientAddress(address.serviceId.equals(this.serviceId) || address.pni.equals(this.serviceId) - ? Optional.empty() - : this.serviceId, - address.pni.equals(this.pni) || address.serviceId.equals(this.pni) ? Optional.empty() : this.pni, + return new RecipientAddress(address.aci.equals(this.aci) ? Optional.empty() : this.aci, + address.pni.equals(this.pni) ? Optional.empty() : this.pni, address.number.equals(this.number) ? Optional.empty() : this.number, address.username.equals(this.username) ? Optional.empty() : this.username); } + public Optional serviceId() { + return aci.map(aci -> (ServiceId) aci).or(this::pni); + } + public String getIdentifier() { - if (serviceId.isPresent()) { - return serviceId.get().toString(); + if (aci.isPresent()) { + return aci.get().toString(); + } else if (pni.isPresent()) { + return pni.get().toString(); } else if (number.isPresent()) { return number.get(); } else { @@ -102,38 +109,31 @@ public String getIdentifier() { public String getLegacyIdentifier() { if (number.isPresent()) { return number.get(); - } else if (serviceId.isPresent()) { - return serviceId.get().toString(); + } else if (aci.isPresent()) { + return aci.get().toString(); + } else if (pni.isPresent()) { + return pni.get().toString(); } else { throw new AssertionError("Given the checks in the constructor, this should not be possible."); } } public boolean matches(RecipientAddress other) { - return (serviceId.isPresent() && other.serviceId.isPresent() && serviceId.get().equals(other.serviceId.get())) - || ( - pni.isPresent() && other.serviceId.isPresent() && pni.get().equals(other.serviceId.get()) - ) - || ( - serviceId.isPresent() && other.pni.isPresent() && serviceId.get().equals(other.pni.get()) - ) - || ( + return (aci.isPresent() && other.aci.isPresent() && aci.get().equals(other.aci.get())) || ( pni.isPresent() && other.pni.isPresent() && pni.get().equals(other.pni.get()) - ) - || ( + ) || ( number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()) ); } public boolean hasSingleIdentifier() { - final var identifiersCount = serviceId().map(s -> 1).orElse(0) - + number().map(s -> 1).orElse(0) - + username().map(s -> 1).orElse(0); + final var identifiersCount = aci().map(s -> 1).orElse(0) + pni().map(s -> 1).orElse(0) + number().map(s -> 1) + .orElse(0) + username().map(s -> 1).orElse(0); return identifiersCount == 1; } public boolean hasIdentifiersOf(RecipientAddress address) { - return (address.serviceId.isEmpty() || address.serviceId.equals(serviceId) || address.serviceId.equals(pni)) + return (address.aci.isEmpty() || address.aci.equals(aci)) && (address.pni.isEmpty() || address.pni.equals(pni)) && (address.number.isEmpty() || address.number.equals(number)) && (address.username.isEmpty() || address.username.equals(username)); @@ -141,13 +141,12 @@ public boolean hasIdentifiersOf(RecipientAddress address) { public boolean hasAdditionalIdentifiersThan(RecipientAddress address) { return ( - serviceId.isPresent() && ( - address.serviceId.isEmpty() || ( - !address.serviceId.equals(serviceId) && !address.pni.equals(serviceId) - ) + aci.isPresent() && ( + address.aci.isEmpty() || !address.aci.equals(aci) + ) ) || ( - pni.isPresent() && !address.serviceId.equals(pni) && ( + pni.isPresent() && ( address.pni.isEmpty() || !address.pni.equals(pni) ) ) || ( @@ -162,15 +161,11 @@ public boolean hasAdditionalIdentifiersThan(RecipientAddress address) { } public boolean hasOnlyPniAndNumber() { - return pni.isPresent() && serviceId.equals(pni) && number.isPresent(); - } - - public boolean isServiceIdPNI() { - return serviceId.isPresent() && (pni.isPresent() && serviceId.equals(pni)); + return pni.isPresent() && aci.isEmpty() && number.isPresent(); } public SignalServiceAddress toSignalServiceAddress() { - return new SignalServiceAddress(serviceId.orElse(ACI.UNKNOWN), number); + return new SignalServiceAddress(serviceId().orElse(ACI.UNKNOWN), number); } public org.asamk.signal.manager.api.RecipientAddress toApiRecipientAddress() { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 7d0aaf5c95d0a..7d47839ba6ffe 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.Utils; import org.asamk.signal.manager.storage.contacts.ContactsStore; import org.asamk.signal.manager.storage.profiles.ProfileStore; +import org.asamk.signal.manager.util.KeyUtils; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; @@ -17,11 +18,12 @@ import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.api.storage.StorageId; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -36,16 +38,15 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, RecipientTrustedResolver, ContactsStore, ProfileStore { - private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class); + private static final Logger logger = LoggerFactory.getLogger(RecipientStore.class); private static final String TABLE_RECIPIENT = "recipient"; - private static final String SQL_IS_CONTACT = "r.given_name IS NOT NULL OR r.family_name IS NOT NULL OR r.expiration_time > 0 OR r.profile_sharing = TRUE OR r.color IS NOT NULL OR r.blocked = TRUE OR r.archived = TRUE"; + private static final String SQL_IS_CONTACT = "r.given_name IS NOT NULL OR r.family_name IS NOT NULL OR r.nick_name IS NOT NULL OR r.expiration_time > 0 OR r.profile_sharing = TRUE OR r.color IS NOT NULL OR r.blocked = TRUE OR r.archived = TRUE"; private final RecipientMergeHandler recipientMergeHandler; private final SelfAddressProvider selfAddressProvider; private final SelfProfileKeyProvider selfProfileKeyProvider; private final Database database; - private final Object recipientsLock = new Object(); private final Map recipientsMerged = new HashMap<>(); private final Map recipientAddressCache = new HashMap<>(); @@ -56,21 +57,29 @@ public static void createSql(Connection connection) throws SQLException { statement.executeUpdate(""" CREATE TABLE recipient ( _id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_id BLOB UNIQUE, + storage_record BLOB, number TEXT UNIQUE, username TEXT UNIQUE, - uuid BLOB UNIQUE, - pni BLOB UNIQUE, + aci TEXT UNIQUE, + pni TEXT UNIQUE, + unregistered_timestamp INTEGER, profile_key BLOB, profile_key_credential BLOB, + needs_pni_signature INTEGER NOT NULL DEFAULT FALSE, given_name TEXT, family_name TEXT, + nick_name TEXT, color TEXT, expiration_time INTEGER NOT NULL DEFAULT 0, + mute_until INTEGER NOT NULL DEFAULT 0, blocked INTEGER NOT NULL DEFAULT FALSE, archived INTEGER NOT NULL DEFAULT FALSE, profile_sharing INTEGER NOT NULL DEFAULT FALSE, + hide_story INTEGER NOT NULL DEFAULT FALSE, + hidden INTEGER NOT NULL DEFAULT FALSE, profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0, profile_given_name TEXT, @@ -99,18 +108,8 @@ public RecipientStore( } public RecipientAddress resolveRecipientAddress(RecipientId recipientId) { - final var sql = ( - """ - SELECT r.number, r.uuid, r.pni, r.username - FROM %s r - WHERE r._id = ? - """ - ).formatted(TABLE_RECIPIENT); try (final var connection = database.getConnection()) { - try (final var statement = connection.prepareStatement(sql)) { - statement.setLong(1, recipientId.id()); - return Utils.executeQuerySingleRow(statement, this::getRecipientAddressFromResultSet); - } + return resolveRecipientAddress(connection, recipientId); } catch (SQLException e) { throw new RuntimeException("Failed read from recipient store", e); } @@ -165,34 +164,30 @@ public RecipientId resolveRecipient(final String identifier) { } private RecipientId resolveRecipientByNumber(final String number) { - synchronized (recipientsLock) { - final RecipientId recipientId; - try (final var connection = database.getConnection()) { - connection.setAutoCommit(false); - recipientId = resolveRecipientLocked(connection, number); - connection.commit(); - } catch (SQLException e) { - throw new RuntimeException("Failed read recipient store", e); - } - return recipientId; + final RecipientId recipientId; + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); + recipientId = resolveRecipientLocked(connection, number); + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed read recipient store", e); } + return recipientId; } @Override public RecipientId resolveRecipient(final ServiceId serviceId) { - synchronized (recipientsLock) { + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); final var recipientWithAddress = recipientAddressCache.get(serviceId); if (recipientWithAddress != null) { return recipientWithAddress.id(); } - try (final var connection = database.getConnection()) { - connection.setAutoCommit(false); - final var recipientId = resolveRecipientLocked(connection, serviceId); - connection.commit(); - return recipientId; - } catch (SQLException e) { - throw new RuntimeException("Failed read recipient store", e); - } + final var recipientId = resolveRecipientLocked(connection, serviceId); + connection.commit(); + return recipientId; + } catch (SQLException e) { + throw new RuntimeException("Failed read recipient store", e); } } @@ -259,17 +254,19 @@ public RecipientId resolveRecipientByUsername( } public RecipientId resolveRecipient(RecipientAddress address) { - synchronized (recipientsLock) { - final RecipientId recipientId; - try (final var connection = database.getConnection()) { - connection.setAutoCommit(false); - recipientId = resolveRecipientLocked(connection, address); - connection.commit(); - } catch (SQLException e) { - throw new RuntimeException("Failed read recipient store", e); - } - return recipientId; + final RecipientId recipientId; + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); + recipientId = resolveRecipientLocked(connection, address); + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed read recipient store", e); } + return recipientId; + } + + public RecipientId resolveRecipient(Connection connection, RecipientAddress address) throws SQLException { + return resolveRecipientLocked(connection, address); } @Override @@ -277,26 +274,34 @@ public RecipientId resolveSelfRecipientTrusted(RecipientAddress address) { return resolveRecipientTrusted(address, true); } + @Override public RecipientId resolveRecipientTrusted(RecipientAddress address) { return resolveRecipientTrusted(address, false); } + public RecipientId resolveRecipientTrusted(Connection connection, RecipientAddress address) throws SQLException { + final var pair = resolveRecipientTrustedLocked(connection, address, false); + if (!pair.second().isEmpty()) { + mergeRecipients(connection, pair.first(), pair.second()); + } + return pair.first(); + } + @Override public RecipientId resolveRecipientTrusted(SignalServiceAddress address) { - return resolveRecipientTrusted(new RecipientAddress(address), false); + return resolveRecipientTrusted(new RecipientAddress(address)); } @Override public RecipientId resolveRecipientTrusted( final Optional aci, final Optional pni, final Optional number ) { - final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni); - return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number, Optional.empty()), false); + return resolveRecipientTrusted(new RecipientAddress(aci, pni, number, Optional.empty())); } @Override public RecipientId resolveRecipientTrusted(final ACI aci, final String username) { - return resolveRecipientTrusted(new RecipientAddress(aci, null, null, username), false); + return resolveRecipientTrusted(new RecipientAddress(aci, null, null, username)); } @Override @@ -321,9 +326,9 @@ public Contact getContact(RecipientId recipientId) { public List> getContacts() { final var sql = ( """ - SELECT r._id, r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived + SELECT r._id, r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp FROM %s r - WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s + WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE """ ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT); try (final var connection = database.getConnection()) { @@ -339,12 +344,52 @@ public List> getContacts() { } } + public Recipient getRecipient(Connection connection, RecipientId recipientId) throws SQLException { + final var sql = ( + """ + SELECT r._id, + r.number, r.aci, r.pni, r.username, + r.profile_key, r.profile_key_credential, + r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp, + r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, + r.storage_record + FROM %s r + WHERE r._id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, recipientId.id()); + return Utils.executeQuerySingleRow(statement, this::getRecipientFromResultSet); + } + } + + public Recipient getRecipient(Connection connection, StorageId storageId) throws SQLException { + final var sql = ( + """ + SELECT r._id, + r.number, r.aci, r.pni, r.username, + r.profile_key, r.profile_key_credential, + r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp, + r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, + r.storage_record + FROM %s r + WHERE r.storage_id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, storageId.getRaw()); + return Utils.executeQuerySingleRow(statement, this::getRecipientFromResultSet); + } + } + public List getRecipients( boolean onlyContacts, Optional blocked, Set recipientIds, Optional name ) { final var sqlWhere = new ArrayList(); if (onlyContacts) { + sqlWhere.add("r.unregistered_timestamp IS NULL"); sqlWhere.add("(" + SQL_IS_CONTACT + ")"); + sqlWhere.add("r.hidden = FALSE"); } if (blocked.isPresent()) { sqlWhere.add("r.blocked = ?"); @@ -358,15 +403,16 @@ public List getRecipients( final var sql = ( """ SELECT r._id, - r.number, r.uuid, r.pni, r.username, + r.number, r.aci, r.pni, r.username, r.profile_key, r.profile_key_credential, - r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived, - r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities + r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp, + r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, + r.storage_record FROM %s r - WHERE (r.number IS NOT NULL OR r.uuid IS NOT NULL) AND %s + WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s """ ).formatted(TABLE_RECIPIENT, sqlWhere.isEmpty() ? "TRUE" : String.join(" AND ", sqlWhere)); - final var selfServiceId = selfAddressProvider.getSelfAddress().serviceId(); + final var selfAddress = selfAddressProvider.getSelfAddress(); try (final var connection = database.getConnection()) { try (final var statement = connection.prepareStatement(sql)) { if (blocked.isPresent()) { @@ -376,7 +422,7 @@ public List getRecipients( return result.filter(r -> name.isEmpty() || ( r.getContact() != null && name.get().equals(r.getContact().getName()) ) || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName()))).map(r -> { - if (r.getAddress().serviceId().equals(selfServiceId)) { + if (r.getAddress().matches(selfAddress)) { return Recipient.newBuilder(r) .withProfileKey(selfProfileKeyProvider.getSelfProfileKey()) .build(); @@ -422,21 +468,21 @@ public Set getAllNumbers() { public Map getServiceIdToProfileKeyMap() { final var sql = ( """ - SELECT r.uuid, r.profile_key + SELECT r.aci, r.profile_key FROM %s r - WHERE r.uuid IS NOT NULL AND r.profile_key IS NOT NULL + WHERE r.aci IS NOT NULL AND r.profile_key IS NOT NULL """ ).formatted(TABLE_RECIPIENT); - final var selfServiceId = selfAddressProvider.getSelfAddress().serviceId().orElse(null); + final var selfAci = selfAddressProvider.getSelfAddress().aci().orElse(null); try (final var connection = database.getConnection()) { try (final var statement = connection.prepareStatement(sql)) { return Utils.executeQueryForStream(statement, resultSet -> { - final var serviceId = ServiceId.parseOrThrow(resultSet.getBytes("uuid")); - if (serviceId.equals(selfServiceId)) { - return new Pair<>(serviceId, selfProfileKeyProvider.getSelfProfileKey()); + final var aci = ACI.parseOrThrow(resultSet.getString("aci")); + if (aci.equals(selfAci)) { + return new Pair<>(aci, selfProfileKeyProvider.getSelfProfileKey()); } final var profileKey = getProfileKeyFromResultSet(resultSet); - return new Pair<>(serviceId, profileKey); + return new Pair<>(aci, profileKey); }).filter(Objects::nonNull).collect(Collectors.toMap(Pair::first, Pair::second)); } } catch (SQLException e) { @@ -444,6 +490,53 @@ public Map getServiceIdToProfileKeyMap() { } } + public List getRecipientIds(Connection connection) throws SQLException { + final var sql = ( + """ + SELECT r._id + FROM %s r + WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + return Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet).toList(); + } + } + + public void setMissingStorageIds() { + final var selectSql = ( + """ + SELECT r._id + FROM %s r + WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL + """ + ).formatted(TABLE_RECIPIENT); + final var updateSql = ( + """ + UPDATE %s + SET storage_id = ? + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); + try (final var selectStmt = connection.prepareStatement(selectSql)) { + final var recipientIds = Utils.executeQueryForStream(selectStmt, this::getRecipientIdFromResultSet) + .toList(); + try (final var updateStmt = connection.prepareStatement(updateSql)) { + for (final var recipientId : recipientIds) { + updateStmt.setBytes(1, KeyUtils.createRawStorageId()); + updateStmt.setLong(2, recipientId.id()); + updateStmt.executeUpdate(); + } + } + } + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed update recipient store", e); + } + } + @Override public void deleteContact(RecipientId recipientId) { storeContact(recipientId, null); @@ -451,19 +544,17 @@ public void deleteContact(RecipientId recipientId) { public void deleteRecipientData(RecipientId recipientId) { logger.debug("Deleting recipient data for {}", recipientId); - synchronized (recipientsLock) { + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId)); - try (final var connection = database.getConnection()) { - connection.setAutoCommit(false); - storeContact(connection, recipientId, null); - storeProfile(connection, recipientId, null); - storeProfileKey(connection, recipientId, null, false); - storeExpiringProfileKeyCredential(connection, recipientId, null); - deleteRecipient(connection, recipientId); - connection.commit(); - } catch (SQLException e) { - throw new RuntimeException("Failed update recipient store", e); - } + storeContact(connection, recipientId, null); + storeProfile(connection, recipientId, null); + storeProfileKey(connection, recipientId, null, false); + storeExpiringProfileKeyCredential(connection, recipientId, null); + deleteRecipient(connection, recipientId); + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed update recipient store", e); } } @@ -506,12 +597,18 @@ public void storeProfile(RecipientId recipientId, final Profile profile) { @Override public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) { try (final var connection = database.getConnection()) { - storeProfileKey(connection, recipientId, profileKey, true); + storeProfileKey(connection, recipientId, profileKey); } catch (SQLException e) { throw new RuntimeException("Failed update recipient store", e); } } + public void storeProfileKey( + Connection connection, RecipientId recipientId, final ProfileKey profileKey + ) throws SQLException { + storeProfileKey(connection, recipientId, profileKey, true); + } + @Override public void storeExpiringProfileKeyCredential( RecipientId recipientId, final ExpiringProfileKeyCredential profileKeyCredential @@ -523,12 +620,138 @@ public void storeExpiringProfileKeyCredential( } } + public void rotateSelfStorageId() { + try (final var connection = database.getConnection()) { + rotateSelfStorageId(connection); + } catch (SQLException e) { + throw new RuntimeException("Failed update recipient store", e); + } + } + + public void rotateSelfStorageId(final Connection connection) throws SQLException { + final var selfRecipientId = resolveRecipient(connection, selfAddressProvider.getSelfAddress()); + rotateStorageId(connection, selfRecipientId); + } + + public StorageId rotateStorageId(final Connection connection, final ServiceId serviceId) throws SQLException { + final var selfRecipientId = resolveRecipient(connection, new RecipientAddress(serviceId)); + return rotateStorageId(connection, selfRecipientId); + } + + public List getStorageIds(Connection connection) throws SQLException { + final var sql = """ + SELECT r.storage_id + FROM %s r WHERE r.storage_id IS NOT NULL AND r._id != ? AND (r.aci IS NOT NULL OR r.pni IS NOT NULL) + """.formatted(TABLE_RECIPIENT); + final var selfRecipientId = resolveRecipient(connection, selfAddressProvider.getSelfAddress()); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, selfRecipientId.id()); + return Utils.executeQueryForStream(statement, this::getContactStorageIdFromResultSet).toList(); + } + } + + public void updateStorageId( + Connection connection, RecipientId recipientId, StorageId storageId + ) throws SQLException { + final var sql = ( + """ + UPDATE %s + SET storage_id = ? + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBytes(1, storageId.getRaw()); + statement.setLong(2, recipientId.id()); + statement.executeUpdate(); + } + } + + public void updateStorageIds(Connection connection, Map storageIdMap) throws SQLException { + final var sql = ( + """ + UPDATE %s + SET storage_id = ? + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + for (final var entry : storageIdMap.entrySet()) { + statement.setBytes(1, entry.getValue().getRaw()); + statement.setLong(2, entry.getKey().id()); + statement.executeUpdate(); + } + } + } + + public StorageId getSelfStorageId(final Connection connection) throws SQLException { + final var selfRecipientId = resolveRecipient(connection, selfAddressProvider.getSelfAddress()); + return StorageId.forAccount(getStorageId(connection, selfRecipientId).getRaw()); + } + + public StorageId getStorageId(final Connection connection, final RecipientId recipientId) throws SQLException { + final var sql = """ + SELECT r.storage_id + FROM %s r WHERE r._id = ? AND r.storage_id IS NOT NULL + """.formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, recipientId.id()); + final var storageId = Utils.executeQueryForOptional(statement, this::getContactStorageIdFromResultSet); + if (storageId.isPresent()) { + return storageId.get(); + } + } + return rotateStorageId(connection, recipientId); + } + + private StorageId rotateStorageId(final Connection connection, final RecipientId recipientId) throws SQLException { + final var newStorageId = StorageId.forAccount(KeyUtils.createRawStorageId()); + updateStorageId(connection, recipientId, newStorageId); + return newStorageId; + } + + public void storeStorageRecord( + final Connection connection, + final RecipientId recipientId, + final StorageId storageId, + final byte[] storageRecord + ) throws SQLException { + final var deleteSql = ( + """ + UPDATE %s + SET storage_id = NULL + WHERE storage_id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(deleteSql)) { + statement.setBytes(1, storageId.getRaw()); + statement.executeUpdate(); + } + final var insertSql = ( + """ + UPDATE %s + SET storage_id = ?, storage_record = ? + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(insertSql)) { + statement.setBytes(1, storageId.getRaw()); + if (storageRecord == null) { + statement.setNull(2, Types.BLOB); + } else { + statement.setBytes(2, storageRecord); + } + statement.setLong(3, recipientId.id()); + statement.executeUpdate(); + } + } + void addLegacyRecipients(final Map recipients) { logger.debug("Migrating legacy recipients to database"); long start = System.nanoTime(); final var sql = ( """ - INSERT INTO %s (_id, number, uuid) + INSERT INTO %s (_id, number, aci) VALUES (?, ?, ?) """ ).formatted(TABLE_RECIPIENT); @@ -541,12 +764,7 @@ void addLegacyRecipients(final Map recipients) { for (final var recipient : recipients.values()) { statement.setLong(1, recipient.getRecipientId().id()); statement.setString(2, recipient.getAddress().number().orElse(null)); - statement.setBytes(3, - recipient.getAddress() - .serviceId() - .map(ServiceId::getRawUuid) - .map(UuidUtil::toByteArray) - .orElse(null)); + statement.setString(3, recipient.getAddress().aci().map(ACI::toString).orElse(null)); statement.executeUpdate(); } } @@ -584,25 +802,157 @@ long getActualRecipientId(long recipientId) { return recipientId; } - private void storeContact( + public void storeContact( final Connection connection, final RecipientId recipientId, final Contact contact ) throws SQLException { final var sql = ( """ UPDATE %s - SET given_name = ?, family_name = ?, expiration_time = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ? + SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ? WHERE _id = ? """ ).formatted(TABLE_RECIPIENT); try (final var statement = connection.prepareStatement(sql)) { - statement.setString(1, contact == null ? null : contact.getGivenName()); - statement.setString(2, contact == null ? null : contact.getFamilyName()); - statement.setInt(3, contact == null ? 0 : contact.getMessageExpirationTime()); - statement.setBoolean(4, contact != null && contact.isProfileSharingEnabled()); - statement.setString(5, contact == null ? null : contact.getColor()); - statement.setBoolean(6, contact != null && contact.isBlocked()); - statement.setBoolean(7, contact != null && contact.isArchived()); - statement.setLong(8, recipientId.id()); + statement.setString(1, contact == null ? null : contact.givenName()); + statement.setString(2, contact == null ? null : contact.familyName()); + statement.setString(3, contact == null ? null : contact.nickName()); + statement.setInt(4, contact == null ? 0 : contact.messageExpirationTime()); + statement.setLong(5, contact == null ? 0 : contact.muteUntil()); + statement.setBoolean(6, contact != null && contact.hideStory()); + statement.setBoolean(7, contact != null && contact.isProfileSharingEnabled()); + statement.setString(8, contact == null ? null : contact.color()); + statement.setBoolean(9, contact != null && contact.isBlocked()); + statement.setBoolean(10, contact != null && contact.isArchived()); + if (contact == null || contact.unregisteredTimestamp() == null) { + statement.setNull(11, Types.INTEGER); + } else { + statement.setLong(11, contact.unregisteredTimestamp()); + } + statement.setLong(12, recipientId.id()); + statement.executeUpdate(); + } + if (contact != null && contact.unregisteredTimestamp() != null) { + markUnregisteredAndSplitIfNecessary(connection, recipientId); + } + rotateStorageId(connection, recipientId); + } + + public int removeStorageIdsFromLocalOnlyUnregisteredRecipients( + final Connection connection, final List storageIds + ) throws SQLException { + final var sql = ( + """ + UPDATE %s + SET storage_id = NULL + WHERE storage_id = ? AND unregistered_timestamp IS NOT NULL + """ + ).formatted(TABLE_RECIPIENT); + var count = 0; + try (final var statement = connection.prepareStatement(sql)) { + for (final var storageId : storageIds) { + statement.setBytes(1, storageId.getRaw()); + count += statement.executeUpdate(); + } + } + return count; + } + + public void markNeedsPniSignature(final RecipientId recipientId, final boolean value) { + logger.debug("Marking {} numbers as need pni signature = {}", recipientId, value); + try (final var connection = database.getConnection()) { + final var sql = ( + """ + UPDATE %s + SET needs_pni_signature = ? + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBoolean(1, value); + statement.setLong(2, recipientId.id()); + statement.executeUpdate(); + } + } catch (SQLException e) { + throw new RuntimeException("Failed update recipient store", e); + } + } + + public boolean needsPniSignature(final RecipientId recipientId) { + try (final var connection = database.getConnection()) { + final var sql = ( + """ + SELECT needs_pni_signature + FROM %s + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, recipientId.id()); + return Utils.executeQuerySingleRow(statement, resultSet -> resultSet.getBoolean("needs_pni_signature")); + } + } catch (SQLException e) { + throw new RuntimeException("Failed read recipient store", e); + } + } + + public void markUnregistered(final Set unregisteredUsers) { + logger.debug("Marking {} numbers as unregistered", unregisteredUsers.size()); + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); + for (final var number : unregisteredUsers) { + final var recipient = findByNumber(connection, number); + if (recipient.isPresent()) { + final var recipientId = recipient.get().id(); + markUnregisteredAndSplitIfNecessary(connection, recipientId); + } + } + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed update recipient store", e); + } + } + + private void markUnregisteredAndSplitIfNecessary( + final Connection connection, final RecipientId recipientId + ) throws SQLException { + markUnregistered(connection, recipientId); + final var address = resolveRecipientAddress(connection, recipientId); + if (address.aci().isPresent() && address.pni().isPresent()) { + final var numberAddress = new RecipientAddress(address.pni().get(), address.number().orElse(null)); + updateRecipientAddress(connection, recipientId, address.removeIdentifiersFrom(numberAddress)); + addNewRecipient(connection, numberAddress); + } + } + + private void markRegistered( + final Connection connection, final RecipientId recipientId + ) throws SQLException { + final var sql = ( + """ + UPDATE %s + SET unregistered_timestamp = NULL + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, recipientId.id()); + statement.executeUpdate(); + } + } + + private void markUnregistered( + final Connection connection, final RecipientId recipientId + ) throws SQLException { + final var sql = ( + """ + UPDATE %s + SET unregistered_timestamp = ? + WHERE _id = ? AND unregistered_timestamp IS NULL + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, System.currentTimeMillis()); + statement.setLong(2, recipientId.id()); statement.executeUpdate(); } } @@ -626,7 +976,7 @@ private void storeExpiringProfileKeyCredential( } } - private void storeProfile( + public void storeProfile( final Connection connection, final RecipientId recipientId, final Profile profile ) throws SQLException { final var sql = ( @@ -652,6 +1002,7 @@ private void storeProfile( statement.setLong(10, recipientId.id()); statement.executeUpdate(); } + rotateStorageId(connection, recipientId); } private void storeProfileKey( @@ -683,55 +1034,85 @@ private void storeProfileKey( statement.setLong(2, recipientId.id()); statement.executeUpdate(); } + rotateStorageId(connection, recipientId); + } + + private RecipientAddress resolveRecipientAddress( + final Connection connection, final RecipientId recipientId + ) throws SQLException { + final var sql = ( + """ + SELECT r.number, r.aci, r.pni, r.username + FROM %s r + WHERE r._id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, recipientId.id()); + return Utils.executeQuerySingleRow(statement, this::getRecipientAddressFromResultSet); + } } private RecipientId resolveRecipientTrusted(RecipientAddress address, boolean isSelf) { final Pair> pair; - synchronized (recipientsLock) { + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); + pair = resolveRecipientTrustedLocked(connection, address, isSelf); + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed update recipient store", e); + } + + if (!pair.second().isEmpty()) { + logger.debug("Resolved address {}, merging {} other recipients", address, pair.second().size()); try (final var connection = database.getConnection()) { connection.setAutoCommit(false); - if (address.hasSingleIdentifier() || ( - !isSelf && selfAddressProvider.getSelfAddress().matches(address) - )) { - pair = new Pair<>(resolveRecipientLocked(connection, address), List.of()); - } else { - pair = MergeRecipientHelper.resolveRecipientTrustedLocked(new HelperStore(connection), address); - - for (final var toBeMergedRecipientId : pair.second()) { - mergeRecipientsLocked(connection, pair.first(), toBeMergedRecipientId); - } - } + mergeRecipients(connection, pair.first(), pair.second()); connection.commit(); } catch (SQLException e) { throw new RuntimeException("Failed update recipient store", e); } } + return pair.first(); + } - if (!pair.second().isEmpty()) { - try (final var connection = database.getConnection()) { - for (final var toBeMergedRecipientId : pair.second()) { - recipientMergeHandler.mergeRecipients(connection, pair.first(), toBeMergedRecipientId); - deleteRecipient(connection, toBeMergedRecipientId); - synchronized (recipientsLock) { - recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId)); - } - } - } catch (SQLException e) { - throw new RuntimeException("Failed update recipient store", e); + private Pair> resolveRecipientTrustedLocked( + final Connection connection, final RecipientAddress address, final boolean isSelf + ) throws SQLException { + if (address.hasSingleIdentifier() || ( + !isSelf && selfAddressProvider.getSelfAddress().matches(address) + )) { + return new Pair<>(resolveRecipientLocked(connection, address), List.of()); + } else { + final var pair = MergeRecipientHelper.resolveRecipientTrustedLocked(new HelperStore(connection), address); + markRegistered(connection, pair.first()); + + for (final var toBeMergedRecipientId : pair.second()) { + mergeRecipientsLocked(connection, pair.first(), toBeMergedRecipientId); } + return pair; + } + } + + private void mergeRecipients( + final Connection connection, final RecipientId recipientId, final List toBeMergedRecipientIds + ) throws SQLException { + for (final var toBeMergedRecipientId : toBeMergedRecipientIds) { + recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId); + deleteRecipient(connection, toBeMergedRecipientId); + recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId)); } - return pair.first(); } private RecipientId resolveRecipientLocked( Connection connection, RecipientAddress address ) throws SQLException { - final var byServiceId = address.serviceId().isEmpty() + final var byAci = address.aci().isEmpty() ? Optional.empty() - : findByServiceId(connection, address.serviceId().get()); + : findByServiceId(connection, address.aci().get()); - if (byServiceId.isPresent()) { - return byServiceId.get().id(); + if (byAci.isPresent()) { + return byAci.get().id(); } final var byPni = address.pni().isEmpty() @@ -775,7 +1156,7 @@ private RecipientId resolveRecipientLocked(Connection connection, String number) if (recipient.isEmpty()) { logger.debug("Got new recipient, number is unknown"); - return addNewRecipient(connection, new RecipientAddress(null, number)); + return addNewRecipient(connection, new RecipientAddress(number)); } return recipient.get().id(); @@ -786,16 +1167,15 @@ private RecipientId addNewRecipient( ) throws SQLException { final var sql = ( """ - INSERT INTO %s (number, uuid, pni, username) + INSERT INTO %s (number, aci, pni, username) VALUES (?, ?, ?, ?) RETURNING _id """ ).formatted(TABLE_RECIPIENT); try (final var statement = connection.prepareStatement(sql)) { statement.setString(1, address.number().orElse(null)); - statement.setBytes(2, - address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null)); - statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null)); + statement.setString(2, address.aci().map(ACI::toString).orElse(null)); + statement.setString(3, address.pni().map(PNI::toString).orElse(null)); statement.setString(4, address.username().orElse(null)); final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper); if (generatedKey.isPresent()) { @@ -809,44 +1189,40 @@ private RecipientId addNewRecipient( } private void removeRecipientAddress(Connection connection, RecipientId recipientId) throws SQLException { - synchronized (recipientsLock) { - recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId)); - final var sql = ( - """ - UPDATE %s - SET number = NULL, uuid = NULL, pni = NULL, username = NULL - WHERE _id = ? - """ - ).formatted(TABLE_RECIPIENT); - try (final var statement = connection.prepareStatement(sql)) { - statement.setLong(1, recipientId.id()); - statement.executeUpdate(); - } + recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId)); + final var sql = ( + """ + UPDATE %s + SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, recipientId.id()); + statement.executeUpdate(); } } private void updateRecipientAddress( Connection connection, RecipientId recipientId, final RecipientAddress address ) throws SQLException { - synchronized (recipientsLock) { - recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId)); - final var sql = ( - """ - UPDATE %s - SET number = ?, uuid = ?, pni = ?, username = ? - WHERE _id = ? - """ - ).formatted(TABLE_RECIPIENT); - try (final var statement = connection.prepareStatement(sql)) { - statement.setString(1, address.number().orElse(null)); - statement.setBytes(2, - address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null)); - statement.setBytes(3, address.pni().map(PNI::getRawUuid).map(UuidUtil::toByteArray).orElse(null)); - statement.setString(4, address.username().orElse(null)); - statement.setLong(5, recipientId.id()); - statement.executeUpdate(); - } + recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId)); + final var sql = ( + """ + UPDATE %s + SET number = ?, aci = ?, pni = ?, username = ? + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setString(1, address.number().orElse(null)); + statement.setString(2, address.aci().map(ACI::toString).orElse(null)); + statement.setString(3, address.pni().map(PNI::toString).orElse(null)); + statement.setString(4, address.username().orElse(null)); + statement.setLong(5, recipientId.id()); + statement.executeUpdate(); } + rotateStorageId(connection, recipientId); } private void deleteRecipient(final Connection connection, final RecipientId recipientId) throws SQLException { @@ -897,7 +1273,7 @@ private Optional findByNumber( final Connection connection, final String number ) throws SQLException { final var sql = """ - SELECT r._id, r.number, r.uuid, r.pni, r.username + SELECT r._id, r.number, r.aci, r.pni, r.username FROM %s r WHERE r.number = ? LIMIT 1 @@ -912,7 +1288,7 @@ private Optional findByUsername( final Connection connection, final String username ) throws SQLException { final var sql = """ - SELECT r._id, r.number, r.uuid, r.pni, r.username + SELECT r._id, r.number, r.aci, r.pni, r.username FROM %s r WHERE r.username = ? LIMIT 1 @@ -931,13 +1307,13 @@ private Optional findByServiceId( return recipientWithAddress; } final var sql = """ - SELECT r._id, r.number, r.uuid, r.pni, r.username + SELECT r._id, r.number, r.aci, r.pni, r.username FROM %s r - WHERE r.uuid = ?1 OR r.pni = ?1 + WHERE %s = ?1 LIMIT 1 - """.formatted(TABLE_RECIPIENT); + """.formatted(TABLE_RECIPIENT, serviceId instanceof ACI ? "r.aci" : "r.pni"); try (final var statement = connection.prepareStatement(sql)) { - statement.setBytes(1, UuidUtil.toByteArray(serviceId.getRawUuid())); + statement.setString(1, serviceId.toString()); recipientWithAddress = Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet); recipientWithAddress.ifPresent(r -> recipientAddressCache.put(serviceId, r)); return recipientWithAddress; @@ -948,17 +1324,16 @@ private Set findAllByAddress( final Connection connection, final RecipientAddress address ) throws SQLException { final var sql = """ - SELECT r._id, r.number, r.uuid, r.pni, r.username + SELECT r._id, r.number, r.aci, r.pni, r.username FROM %s r - WHERE r.uuid = ?1 OR r.pni = ?1 OR - r.uuid = ?2 OR r.pni = ?2 OR + WHERE r.aci = ?1 OR + r.pni = ?2 OR r.number = ?3 OR r.username = ?4 """.formatted(TABLE_RECIPIENT); try (final var statement = connection.prepareStatement(sql)) { - statement.setBytes(1, - address.serviceId().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null)); - statement.setBytes(2, address.pni().map(ServiceId::getRawUuid).map(UuidUtil::toByteArray).orElse(null)); + statement.setString(1, address.aci().map(ServiceId::toString).orElse(null)); + statement.setString(2, address.pni().map(ServiceId::toString).orElse(null)); statement.setString(3, address.number().orElse(null)); statement.setString(4, address.username().orElse(null)); return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet) @@ -969,7 +1344,7 @@ private Set findAllByAddress( private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException { final var sql = ( """ - SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived + SELECT r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp FROM %s r WHERE r._id = ? AND (%s) """ @@ -1015,7 +1390,7 @@ private ExpiringProfileKeyCredential getExpiringProfileKeyCredential( } } - private Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException { + public Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException { final var sql = ( """ SELECT r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities @@ -1030,13 +1405,11 @@ private Profile getProfile(final Connection connection, final RecipientId recipi } private RecipientAddress getRecipientAddressFromResultSet(ResultSet resultSet) throws SQLException { - final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(UuidUtil::parseOrNull).map(PNI::from); - final var serviceIdUuid = Optional.ofNullable(resultSet.getBytes("uuid")).map(UuidUtil::parseOrNull); - final var serviceId = serviceIdUuid.isPresent() && pni.isPresent() && serviceIdUuid.get() - .equals(pni.get().getRawUuid()) ? pni.map(p -> p) : serviceIdUuid.map(ACI::from); + final var aci = Optional.ofNullable(resultSet.getString("aci")).map(ACI::parseOrNull); + final var pni = Optional.ofNullable(resultSet.getString("pni")).map(PNI::parseOrNull); final var number = Optional.ofNullable(resultSet.getString("number")); final var username = Optional.ofNullable(resultSet.getString("username")); - return new RecipientAddress(serviceId, pni, number, username); + return new RecipientAddress(aci, pni, number, username); } private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException { @@ -1054,17 +1427,24 @@ private Recipient getRecipientFromResultSet(final ResultSet resultSet) throws SQ getContactFromResultSet(resultSet), getProfileKeyFromResultSet(resultSet), getExpiringProfileKeyCredentialFromResultSet(resultSet), - getProfileFromResultSet(resultSet)); + getProfileFromResultSet(resultSet), + getStorageRecordFromResultSet(resultSet)); } private Contact getContactFromResultSet(ResultSet resultSet) throws SQLException { + final var unregisteredTimestamp = resultSet.getLong("unregistered_timestamp"); return new Contact(resultSet.getString("given_name"), resultSet.getString("family_name"), + resultSet.getString("nick_name"), resultSet.getString("color"), resultSet.getInt("expiration_time"), + resultSet.getLong("mute_until"), + resultSet.getBoolean("hide_story"), resultSet.getBoolean("blocked"), resultSet.getBoolean("archived"), - resultSet.getBoolean("profile_sharing")); + resultSet.getBoolean("profile_sharing"), + resultSet.getBoolean("hidden"), + unregisteredTimestamp == 0 ? null : unregisteredTimestamp); } private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException { @@ -1114,6 +1494,15 @@ private ExpiringProfileKeyCredential getExpiringProfileKeyCredentialFromResultSe } } + private StorageId getContactStorageIdFromResultSet(ResultSet resultSet) throws SQLException { + final var storageId = resultSet.getBytes("storage_id"); + return StorageId.forContact(storageId); + } + + private byte[] getStorageRecordFromResultSet(ResultSet resultSet) throws SQLException { + return resultSet.getBytes("storage_record"); + } + public interface RecipientMergeHandler { void mergeRecipients( diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java index 21708551ac784..598ef791f8dd8 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/recipients/RecipientTrustedResolver.java @@ -11,6 +11,8 @@ public interface RecipientTrustedResolver { RecipientId resolveSelfRecipientTrusted(RecipientAddress address); + RecipientId resolveRecipientTrusted(RecipientAddress address); + RecipientId resolveRecipientTrusted(SignalServiceAddress address); RecipientId resolveRecipientTrusted(Optional aci, Optional pni, Optional number); @@ -30,6 +32,11 @@ public RecipientId resolveSelfRecipientTrusted(final RecipientAddress address) { return recipientTrustedResolverSupplier.get().resolveSelfRecipientTrusted(address); } + @Override + public RecipientId resolveRecipientTrusted(final RecipientAddress address) { + return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(address); + } + @Override public RecipientId resolveRecipientTrusted(final SignalServiceAddress address) { return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(address); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java index 751f345bed1cb..b9d872e84eb48 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java @@ -55,9 +55,9 @@ public MessageSendLogStore(final Database database, final boolean disableMessage logger.debug("Stopping msl cleanup thread"); } }); - cleanupThread.setName("msl-cleanup"); - cleanupThread.setDaemon(true); - cleanupThread.start(); + this.cleanupThread.setName("msl-cleanup"); + this.cleanupThread.setDaemon(true); + this.cleanupThread.start(); } public static void createSql(Connection connection) throws SQLException { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java index fe49d91bb6162..4bde8b52086ae 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java @@ -1,5 +1,14 @@ package org.asamk.signal.manager.storage.senderKeys; +import org.asamk.signal.manager.api.Pair; +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.groups.state.SenderKeyRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -12,21 +21,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.asamk.signal.manager.api.Pair; -import org.asamk.signal.manager.helper.RecipientAddressResolver; -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.asamk.signal.manager.storage.recipients.RecipientResolver; -import org.signal.libsignal.protocol.InvalidMessageException; -import org.signal.libsignal.protocol.groups.state.SenderKeyRecord; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class LegacySenderKeyRecordStore { - private final static Logger logger = LoggerFactory.getLogger(LegacySenderKeyRecordStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacySenderKeyRecordStore.class); - public static void migrate(final File senderKeysPath, final RecipientResolver resolver, - final RecipientAddressResolver addressResolver, final SenderKeyStore senderKeyStore) { + public static void migrate( + final File senderKeysPath, + final RecipientResolver resolver, + final RecipientAddressResolver addressResolver, + final SenderKeyStore senderKeyStore + ) { final var files = senderKeysPath.listFiles(); if (files == null) { return; @@ -67,18 +71,22 @@ private static void deleteAllSenderKeys(File senderKeysPath) { } } - final static Pattern senderKeyFileNamePattern = Pattern.compile("(\\d+)_(\\d+)_([\\da-z\\-]+)"); + static final Pattern senderKeyFileNamePattern = Pattern.compile("(\\d+)_(\\d+)_([\\da-z\\-]+)"); private static List parseFileNames(final File[] files, final RecipientResolver resolver) { - return Arrays.stream(files).map(f -> senderKeyFileNamePattern.matcher(f.getName())).filter(Matcher::matches) + return (List) Arrays.stream(files) + .map(f -> senderKeyFileNamePattern.matcher(f.getName())) + .filter(Matcher::matches) .map(matcher -> { final var recipientId = resolver.resolveRecipient(Long.parseLong(matcher.group(1))); if (recipientId == null) { return Optional.empty(); } - return Optional.of(new Key(recipientId, Integer.parseInt(matcher.group(2)), - UUID.fromString(matcher.group(3)))); - }).map(Optional::get).filter(Objects::nonNull).toList(); + return Optional.of(new Key(recipientId, Integer.parseInt(matcher.group(2)), UUID.fromString(matcher.group(3)))); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); } private static File getSenderKeyFile(Key key, final File senderKeysPath) { @@ -99,6 +107,5 @@ private static SenderKeyRecord loadSenderKeyLocked(final Key key, final File sen } } - record Key(RecipientId recipientId, int deviceId, UUID distributionId) { - } + record Key(RecipientId recipientId, int deviceId, UUID distributionId) {} } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java index bbd05dea9d6f9..cda652b23ce43 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java @@ -19,7 +19,7 @@ public class LegacySenderKeySharedStore { - private final static Logger logger = LoggerFactory.getLogger(LegacySenderKeySharedStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacySenderKeySharedStore.class); public static void migrate( final File file, diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java index e321e9eafaefe..39e1df9710758 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java @@ -20,8 +20,8 @@ public class SenderKeyRecordStore implements SenderKeyStore { - private final static Logger logger = LoggerFactory.getLogger(SenderKeyRecordStore.class); - private final static String TABLE_SENDER_KEY = "sender_key"; + private static final Logger logger = LoggerFactory.getLogger(SenderKeyRecordStore.class); + private static final String TABLE_SENDER_KEY = "sender_key"; private final Database database; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java index 0b17d479147d3..284ba08f0c24b 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java @@ -19,8 +19,8 @@ public class SenderKeySharedStore { - private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class); - private final static String TABLE_SENDER_KEY_SHARED = "sender_key_shared"; + private static final Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class); + private static final String TABLE_SENDER_KEY_SHARED = "sender_key_shared"; private final Database database; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java index bce6a8e139d04..5556ed79dc634 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java @@ -1,5 +1,14 @@ package org.asamk.signal.manager.storage.sessions; +import org.asamk.signal.manager.api.Pair; +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.util.IOUtils; +import org.signal.libsignal.protocol.state.SessionRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -12,21 +21,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.asamk.signal.manager.api.Pair; -import org.asamk.signal.manager.helper.RecipientAddressResolver; -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.asamk.signal.manager.storage.recipients.RecipientResolver; -import org.asamk.signal.manager.util.IOUtils; -import org.signal.libsignal.protocol.state.SessionRecord; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class LegacySessionStore { - private final static Logger logger = LoggerFactory.getLogger(LegacySessionStore.class); + private static final Logger logger = LoggerFactory.getLogger(LegacySessionStore.class); - public static void migrate(final File sessionsPath, final RecipientResolver resolver, - final RecipientAddressResolver addressResolver, final SessionStore sessionStore) { + public static void migrate( + final File sessionsPath, + final RecipientResolver resolver, + final RecipientAddressResolver addressResolver, + final SessionStore sessionStore + ) { final var keys = getKeysLocked(sessionsPath, resolver); final var sessions = keys.stream().map(key -> { final var record = loadSessionLocked(key, sessionsPath); @@ -70,16 +74,20 @@ private static Collection getKeysLocked(File sessionsPath, final RecipientR static final Pattern sessionFileNamePattern = Pattern.compile("(\\d+)_(\\d+)"); - @SuppressWarnings("null") private static List parseFileNames(final File[] files, final RecipientResolver resolver) { - return Arrays.stream(files).map(f -> sessionFileNamePattern.matcher(f.getName())).filter(Matcher::matches) + return (List) Arrays.stream(files) + .map(f -> sessionFileNamePattern.matcher(f.getName())) + .filter(Matcher::matches) .map(matcher -> { final var recipientId = resolver.resolveRecipient(Long.parseLong(matcher.group(1))); if (recipientId == null) { return Optional.empty(); } - return Optional.of(new Key(recipientId, Integer.parseInt(matcher.group(2)))); - }).map(Optional::get).filter(Objects::nonNull).toList(); + return Optional.of(new Key(recipientId, Integer.parseInt(matcher.group(2)))); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); } private static File getSessionFile(Key key, final File sessionsPath) { @@ -104,6 +112,5 @@ private static SessionRecord loadSessionLocked(final Key key, final File session } } - record Key(RecipientId recipientId, int deviceId) { - } + record Key(RecipientId recipientId, int deviceId) {} } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 852e893b0bf37..3f3ba4d670992 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -27,7 +27,7 @@ public class SessionStore implements SignalServiceSessionStore { private static final String TABLE_SESSION = "session"; - private final static Logger logger = LoggerFactory.getLogger(SessionStore.class); + private static final Logger logger = LoggerFactory.getLogger(SessionStore.class); private final Map cachedSessions = new HashMap<>(); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/LegacyStickerStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/LegacyStickerStore.java index f71e8c793670b..864dce8d29c7a 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/LegacyStickerStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/LegacyStickerStore.java @@ -5,17 +5,13 @@ import java.util.Base64; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; public class LegacyStickerStore { public static void migrate(Storage storage, StickerStore stickerStore) { final var packIds = new HashSet(); - @SuppressWarnings("null") - final var stickers = storage.stickers.stream().map(s -> { - @SuppressWarnings("null") + final List stickers = (List) storage.stickers.stream().map(s -> { var packId = StickerPackId.deserialize(Base64.getDecoder().decode(s.packId)); if (packIds.contains(packId)) { // Remove legacy duplicate packIds ... @@ -24,8 +20,8 @@ public static void migrate(Storage storage, StickerStore stickerStore) { packIds.add(packId); var packKey = Base64.getDecoder().decode(s.packKey); var installed = s.installed; - return Optional.of(new StickerPack(-1, packId, packKey, installed)); - }).map(Optional::get).filter(Objects::nonNull).collect(Collectors.toList()); + return Optional.of(new StickerPack(-1, packId, packKey, installed)); + }).filter(Optional::isPresent).map(Optional::get).toList(); stickerStore.addLegacyStickers(stickers); } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/StickerStore.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/StickerStore.java index aacd18bd57ba9..3222342482034 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/StickerStore.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/storage/stickers/StickerStore.java @@ -14,7 +14,7 @@ public class StickerStore { - private final static Logger logger = LoggerFactory.getLogger(StickerStore.class); + private static final Logger logger = LoggerFactory.getLogger(StickerStore.class); private static final String TABLE_STICKER = "sticker"; private final Database database; diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java new file mode 100644 index 0000000000000..acce82e6ce27b --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java @@ -0,0 +1,222 @@ +package org.asamk.signal.manager.syncStorage; + +import org.asamk.signal.manager.api.Profile; +import org.asamk.signal.manager.internal.JobExecutor; +import org.asamk.signal.manager.jobs.CheckWhoAmIJob; +import org.asamk.signal.manager.jobs.DownloadProfileAvatarJob; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.push.UsernameLinkComponents; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.api.util.OptionalUtil; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Optional; + +/** + * Processes {@link SignalAccountRecord}s. + */ +public class AccountRecordProcessor extends DefaultStorageRecordProcessor { + + private static final Logger logger = LoggerFactory.getLogger(AccountRecordProcessor.class); + private final SignalAccountRecord localAccountRecord; + private final SignalAccount account; + private final Connection connection; + private final JobExecutor jobExecutor; + + public AccountRecordProcessor( + SignalAccount account, Connection connection, final JobExecutor jobExecutor + ) throws SQLException { + this.account = account; + this.connection = connection; + this.jobExecutor = jobExecutor; + final var selfRecipientId = account.getSelfRecipientId(); + final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId); + final var storageId = account.getRecipientStore().getSelfStorageId(connection); + this.localAccountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), + recipient, + account.getUsernameLink(), + storageId.getRaw()).getAccount().get(); + } + + @Override + protected boolean isInvalid(SignalAccountRecord remote) { + return false; + } + + @Override + protected Optional getMatching(SignalAccountRecord record) { + return Optional.of(localAccountRecord); + } + + @Override + protected SignalAccountRecord merge(SignalAccountRecord remote, SignalAccountRecord local) { + String givenName; + String familyName; + if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) { + givenName = remote.getGivenName().orElse(""); + familyName = remote.getFamilyName().orElse(""); + } else { + givenName = local.getGivenName().orElse(""); + familyName = local.getFamilyName().orElse(""); + } + + final var payments = remote.getPayments().getEntropy().isPresent() ? remote.getPayments() : local.getPayments(); + final var subscriber = remote.getSubscriber().getId().isPresent() + ? remote.getSubscriber() + : local.getSubscriber(); + final var storyViewReceiptsState = remote.getStoryViewReceiptsState() == OptionalBool.UNSET + ? local.getStoryViewReceiptsState() + : remote.getStoryViewReceiptsState(); + final var unknownFields = remote.serializeUnknownFields(); + final var avatarUrlPath = OptionalUtil.or(remote.getAvatarUrlPath(), local.getAvatarUrlPath()).orElse(""); + final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null); + final var noteToSelfArchived = remote.isNoteToSelfArchived(); + final var noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread(); + final var readReceipts = remote.isReadReceiptsEnabled(); + final var typingIndicators = remote.isTypingIndicatorsEnabled(); + final var sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled(); + final var linkPreviews = remote.isLinkPreviewsEnabled(); + final var unlisted = remote.isPhoneNumberUnlisted(); + final var pinnedConversations = remote.getPinnedConversations(); + final var phoneNumberSharingMode = remote.getPhoneNumberSharingMode(); + final var preferContactAvatars = remote.isPreferContactAvatars(); + final var universalExpireTimer = remote.getUniversalExpireTimer(); + final var e164 = local.getE164(); + final var defaultReactions = !remote.getDefaultReactions().isEmpty() + ? remote.getDefaultReactions() + : local.getDefaultReactions(); + final var displayBadgesOnProfile = remote.isDisplayBadgesOnProfile(); + final var subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled(); + final var keepMutedChatsArchived = remote.isKeepMutedChatsArchived(); + final var hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy(); + final var hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory(); + final var storiesDisabled = remote.isStoriesDisabled(); + final var hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet() + || local.hasSeenGroupStoryEducationSheet(); + final var username = remote.getUsername(); + final var usernameLink = remote.getUsernameLink(); + + final var mergedBuilder = new SignalAccountRecord.Builder(remote.getId().getRaw(), unknownFields).setGivenName( + givenName) + .setFamilyName(familyName) + .setAvatarUrlPath(avatarUrlPath) + .setProfileKey(profileKey) + .setNoteToSelfArchived(noteToSelfArchived) + .setNoteToSelfForcedUnread(noteToSelfForcedUnread) + .setReadReceiptsEnabled(readReceipts) + .setTypingIndicatorsEnabled(typingIndicators) + .setSealedSenderIndicatorsEnabled(sealedSenderIndicators) + .setLinkPreviewsEnabled(linkPreviews) + .setUnlistedPhoneNumber(unlisted) + .setPhoneNumberSharingMode(phoneNumberSharingMode) + .setPinnedConversations(pinnedConversations) + .setPreferContactAvatars(preferContactAvatars) + .setPayments(payments.isEnabled(), payments.getEntropy().orElse(null)) + .setUniversalExpireTimer(universalExpireTimer) + .setDefaultReactions(defaultReactions) + .setSubscriber(subscriber) + .setDisplayBadgesOnProfile(displayBadgesOnProfile) + .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled) + .setKeepMutedChatsArchived(keepMutedChatsArchived) + .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy) + .setHasViewedOnboardingStory(hasViewedOnboardingStory) + .setStoriesDisabled(storiesDisabled) + .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation) + .setStoryViewReceiptsState(storyViewReceiptsState) + .setUsername(username) + .setUsernameLink(usernameLink) + .setE164(e164); + final var merged = mergedBuilder.build(); + + final var matchesRemote = doProtosMatch(merged, remote); + if (matchesRemote) { + return remote; + } + + final var matchesLocal = doProtosMatch(merged, local); + if (matchesLocal) { + return local; + } + + return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); + } + + @Override + protected void insertLocal(SignalAccountRecord record) { + throw new UnsupportedOperationException( + "We should always have a local AccountRecord, so we should never been inserting a new one."); + } + + @Override + protected void updateLocal(StorageRecordUpdate update) throws SQLException { + final var accountRecord = update.newRecord(); + + if (!accountRecord.getE164().equals(account.getNumber())) { + jobExecutor.enqueueJob(new CheckWhoAmIJob()); + } + + account.getConfigurationStore().setReadReceipts(connection, accountRecord.isReadReceiptsEnabled()); + account.getConfigurationStore().setTypingIndicators(connection, accountRecord.isTypingIndicatorsEnabled()); + account.getConfigurationStore() + .setUnidentifiedDeliveryIndicators(connection, accountRecord.isSealedSenderIndicatorsEnabled()); + account.getConfigurationStore().setLinkPreviews(connection, accountRecord.isLinkPreviewsEnabled()); + account.getConfigurationStore() + .setPhoneNumberSharingMode(connection, + StorageSyncModels.remoteToLocal(accountRecord.getPhoneNumberSharingMode())); + account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountRecord.isPhoneNumberUnlisted()); + + account.setUsername(accountRecord.getUsername() != null && !accountRecord.getUsername().isEmpty() + ? accountRecord.getUsername() + : null); + if (accountRecord.getUsernameLink() != null) { + final var usernameLink = accountRecord.getUsernameLink(); + account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(), + UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray()))); + account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name()); + } + + if (accountRecord.getProfileKey().isPresent()) { + ProfileKey profileKey; + try { + profileKey = new ProfileKey(accountRecord.getProfileKey().get()); + } catch (InvalidInputException e) { + logger.debug("Received invalid profile key from storage"); + profileKey = null; + } + if (profileKey != null) { + account.setProfileKey(profileKey); + final var avatarPath = accountRecord.getAvatarUrlPath().orElse(null); + jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath)); + } + } + + final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId()); + final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + builder.withGivenName(accountRecord.getGivenName().orElse(null)); + builder.withFamilyName(accountRecord.getFamilyName().orElse(null)); + account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build()); + account.getRecipientStore() + .storeStorageRecord(connection, + account.getSelfRecipientId(), + accountRecord.getId(), + accountRecord.toProto().encode()); + } + + @Override + public int compare(SignalAccountRecord lhs, SignalAccountRecord rhs) { + return 0; + } + + private static boolean doProtosMatch(SignalAccountRecord merged, SignalAccountRecord other) { + return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java new file mode 100644 index 0000000000000..5bae64c82969b --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java @@ -0,0 +1,343 @@ +package org.asamk.signal.manager.syncStorage; + +import org.asamk.signal.manager.api.Contact; +import org.asamk.signal.manager.api.Profile; +import org.asamk.signal.manager.internal.JobExecutor; +import org.asamk.signal.manager.jobs.DownloadProfileJob; +import org.asamk.signal.manager.jobs.RefreshRecipientsJob; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.util.KeyUtils; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.push.ServiceId.ACI; +import org.whispersystems.signalservice.api.push.ServiceId.PNI; +import org.whispersystems.signalservice.api.storage.SignalContactRecord; +import org.whispersystems.signalservice.api.util.OptionalUtil; +import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +public class ContactRecordProcessor extends DefaultStorageRecordProcessor { + + private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class); + + private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$"); + + private final ACI selfAci; + private final PNI selfPni; + private final String selfNumber; + private final SignalAccount account; + private final Connection connection; + private final JobExecutor jobExecutor; + + public ContactRecordProcessor(SignalAccount account, Connection connection, final JobExecutor jobExecutor) { + this.account = account; + this.connection = connection; + this.jobExecutor = jobExecutor; + this.selfAci = account.getAci(); + this.selfPni = account.getPni(); + this.selfNumber = account.getNumber(); + } + + /** + * Error cases: + * - You can't have a contact record without an ACI or PNI. + * - You can't have a contact record for yourself. That should be an account record. + */ + @Override + protected boolean isInvalid(SignalContactRecord remote) { + boolean hasAci = remote.getAci().isPresent() && remote.getAci().get().isValid(); + boolean hasPni = remote.getPni().isPresent() && remote.getPni().get().isValid(); + + if (!hasAci && !hasPni) { + logger.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid."); + return true; + } else if (selfAci != null && selfAci.equals(remote.getAci().orElse(null)) || ( + selfPni != null && selfPni.equals(remote.getPni().orElse(null)) + ) || (selfNumber != null && selfNumber.equals(remote.getNumber().orElse(null)))) { + logger.debug("Found a ContactRecord for ourselves -- marking as invalid."); + return true; + } else if (remote.getNumber().isPresent() && !isValidE164(remote.getNumber().get())) { + logger.debug("Found a record with an invalid E164. Marking as invalid."); + return true; + } else { + return false; + } + } + + @Override + protected Optional getMatching(SignalContactRecord remote) throws SQLException { + final var address = getRecipientAddress(remote); + final var recipientId = account.getRecipientStore().resolveRecipient(connection, address); + final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); + + final var identifier = recipient.getAddress().getIdentifier(); + final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, identifier); + final var storageId = account.getRecipientStore().getStorageId(connection, recipientId); + + return Optional.of(StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()) + .getContact() + .get()); + } + + @Override + protected SignalContactRecord merge( + SignalContactRecord remote, SignalContactRecord local + ) { + String profileGivenName; + String profileFamilyName; + if (remote.getProfileGivenName().isPresent() || remote.getProfileFamilyName().isPresent()) { + profileGivenName = remote.getProfileGivenName().orElse(""); + profileFamilyName = remote.getProfileFamilyName().orElse(""); + } else { + profileGivenName = local.getProfileGivenName().orElse(""); + profileFamilyName = local.getProfileFamilyName().orElse(""); + } + + IdentityState identityState; + byte[] identityKey; + if (remote.getIdentityKey().isPresent() && ( + remote.getIdentityState() != local.getIdentityState() + || local.getIdentityKey().isEmpty() + || !account.isPrimaryDevice() + + )) { + identityState = remote.getIdentityState(); + identityKey = remote.getIdentityKey().get(); + } else { + identityState = local.getIdentityState(); + identityKey = local.getIdentityKey().orElse(null); + } + + if (local.getAci().isPresent() + && local.getIdentityKey().isPresent() + && remote.getIdentityKey().isPresent() + && !Arrays.equals(local.getIdentityKey().get(), remote.getIdentityKey().get())) { + logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.", + local.getAci().orElse(null)); + final var address = getRecipientAddress(local); + jobExecutor.enqueueJob(new DownloadProfileJob(address)); + } + + final var e164sMatchButPnisDont = local.getNumber().isPresent() + && local.getNumber() + .get() + .equals(remote.getNumber().orElse(null)) + && local.getPni().isPresent() + && remote.getPni().isPresent() + && !local.getPni().get().equals(remote.getPni().get()); + + final var pnisMatchButE164sDont = local.getPni().isPresent() + && local.getPni() + .get() + .equals(remote.getPni().orElse(null)) + && local.getNumber().isPresent() + && remote.getNumber().isPresent() + && !local.getNumber().get().equals(remote.getNumber().get()); + + PNI pni; + String e164; + if (!account.isPrimaryDevice() && (e164sMatchButPnisDont || pnisMatchButE164sDont)) { + if (e164sMatchButPnisDont) { + logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair."); + } else if (pnisMatchButE164sDont) { + logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair."); + } + jobExecutor.enqueueJob(new RefreshRecipientsJob()); + pni = local.getPni().get(); + e164 = local.getNumber().get(); + } else { + pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null); + e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null); + } + + final var unknownFields = remote.serializeUnknownFields(); + final var aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get(); + final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null); + final var username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse(""); + final var blocked = remote.isBlocked(); + final var profileSharing = remote.isProfileSharingEnabled(); + final var archived = remote.isArchived(); + final var forcedUnread = remote.isForcedUnread(); + final var muteUntil = remote.getMuteUntil(); + final var hideStory = remote.shouldHideStory(); + final var unregisteredTimestamp = remote.getUnregisteredTimestamp(); + final var hidden = remote.isHidden(); + final var systemGivenName = account.isPrimaryDevice() + ? local.getSystemGivenName().orElse("") + : remote.getSystemGivenName().orElse(""); + final var systemFamilyName = account.isPrimaryDevice() + ? local.getSystemFamilyName().orElse("") + : remote.getSystemFamilyName().orElse(""); + final var systemNickname = remote.getSystemNickname().orElse(""); + final var pniSignatureVerified = remote.isPniSignatureVerified() || local.isPniSignatureVerified(); + + final var mergedBuilder = new SignalContactRecord.Builder(remote.getId().getRaw(), aci, unknownFields).setE164( + e164) + .setPni(pni) + .setProfileGivenName(profileGivenName) + .setProfileFamilyName(profileFamilyName) + .setSystemGivenName(systemGivenName) + .setSystemFamilyName(systemFamilyName) + .setSystemNickname(systemNickname) + .setProfileKey(profileKey) + .setUsername(username) + .setIdentityState(identityState) + .setIdentityKey(identityKey) + .setBlocked(blocked) + .setProfileSharingEnabled(profileSharing) + .setArchived(archived) + .setForcedUnread(forcedUnread) + .setMuteUntil(muteUntil) + .setHideStory(hideStory) + .setUnregisteredTimestamp(unregisteredTimestamp) + .setHidden(hidden) + .setPniSignatureVerified(pniSignatureVerified); + final var merged = mergedBuilder.build(); + + final var matchesRemote = doProtosMatch(merged, remote); + if (matchesRemote) { + return remote; + } + + final var matchesLocal = doProtosMatch(merged, local); + if (matchesLocal) { + return local; + } + + return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); + } + + @Override + protected void insertLocal(SignalContactRecord record) throws SQLException { + StorageRecordUpdate update = new StorageRecordUpdate<>(null, record); + updateLocal(update); + } + + @Override + protected void updateLocal(StorageRecordUpdate update) throws SQLException { + final var contactRecord = update.newRecord(); + final var address = getRecipientAddress(contactRecord); + final var recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address); + final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); + + final var contact = recipient.getContact(); + final var blocked = contact != null && contact.isBlocked(); + final var profileShared = contact != null && contact.isProfileSharingEnabled(); + final var archived = contact != null && contact.isArchived(); + final var hidden = contact != null && contact.isHidden(); + final var hideStory = contact != null && contact.hideStory(); + final var muteUntil = contact == null ? 0 : contact.muteUntil(); + final var unregisteredTimestamp = contact == null || contact.unregisteredTimestamp() == null + ? 0 + : contact.unregisteredTimestamp(); + final var contactGivenName = contact == null ? null : contact.givenName(); + final var contactFamilyName = contact == null ? null : contact.familyName(); + final var contactNickName = contact == null ? null : contact.nickName(); + if (blocked != contactRecord.isBlocked() + || profileShared != contactRecord.isProfileSharingEnabled() + || archived != contactRecord.isArchived() + || hidden != contactRecord.isHidden() + || hideStory != contactRecord.shouldHideStory() + || muteUntil != contactRecord.getMuteUntil() + || unregisteredTimestamp != contactRecord.getUnregisteredTimestamp() + || !Objects.equals(contactRecord.getSystemGivenName().orElse(null), contactGivenName) + || !Objects.equals(contactRecord.getSystemFamilyName().orElse(null), contactFamilyName) + || !Objects.equals(contactRecord.getSystemNickname().orElse(null), contactNickName)) { + logger.debug("Storing new or updated contact {}", recipientId); + final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked()) + .withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled()) + .withIsArchived(contactRecord.isArchived()) + .withIsHidden(contactRecord.isHidden()) + .withMuteUntil(contactRecord.getMuteUntil()) + .withHideStory(contactRecord.shouldHideStory()) + .withGivenName(contactRecord.getSystemGivenName().orElse(null)) + .withFamilyName(contactRecord.getSystemFamilyName().orElse(null)) + .withNickName(contactRecord.getSystemNickname().orElse(null)) + .withUnregisteredTimestamp(contactRecord.getUnregisteredTimestamp() == 0 + ? null + : contactRecord.getUnregisteredTimestamp()); + account.getRecipientStore().storeContact(connection, recipientId, newContact.build()); + } + + final var profile = recipient.getProfile(); + final var profileGivenName = profile == null ? null : profile.getGivenName(); + final var profileFamilyName = profile == null ? null : profile.getFamilyName(); + if (!Objects.equals(contactRecord.getProfileGivenName().orElse(null), profileGivenName) || !Objects.equals( + contactRecord.getProfileFamilyName().orElse(null), + profileFamilyName)) { + final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null)) + .withFamilyName(contactRecord.getProfileFamilyName().orElse(null)) + .build(); + account.getRecipientStore().storeProfile(connection, recipientId, newProfile); + } + if (contactRecord.getProfileKey().isPresent()) { + try { + logger.trace("Storing profile key {}", recipientId); + final var profileKey = new ProfileKey(contactRecord.getProfileKey().get()); + account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey); + } catch (InvalidInputException e) { + logger.warn("Received invalid contact profile key from storage"); + } + } + if (contactRecord.getIdentityKey().isPresent() && contactRecord.getAci().isPresent()) { + try { + logger.trace("Storing identity key {}", recipientId); + final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get()); + account.getIdentityKeyStore() + .saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey); + + final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState()); + if (trustLevel != null) { + account.getIdentityKeyStore() + .setIdentityTrustLevel(connection, + contactRecord.getAci().orElse(null), + identityKey, + trustLevel); + } + } catch (InvalidKeyException e) { + logger.warn("Received invalid contact identity key from storage"); + } + } + account.getRecipientStore() + .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode()); + } + + private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) { + return new RecipientAddress(contactRecord.getAci().orElse(null), + contactRecord.getPni().orElse(null), + contactRecord.getNumber().orElse(null), + contactRecord.getUsername().orElse(null)); + } + + @Override + public int compare(SignalContactRecord lhs, SignalContactRecord rhs) { + if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || ( + lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber()) + ) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) { + return 0; + } else { + return 1; + } + } + + private static boolean isValidE164(String value) { + return E164_PATTERN.matcher(value).matches(); + } + + private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) { + return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java new file mode 100644 index 0000000000000..68b5f9cc59943 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java @@ -0,0 +1,98 @@ +package org.asamk.signal.manager.syncStorage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.storage.SignalRecord; +import org.whispersystems.signalservice.api.storage.StorageId; + +import java.sql.SQLException; +import java.util.Comparator; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +/** + * An implementation of {@link StorageRecordProcessor} that solidifies a pattern and reduces + * duplicate code in individual implementations. + *

+ * Concerning the implementation of {@link #compare(Object, Object)}, it's purpose is to detect if + * two items would map to the same logical entity (i.e. they would correspond to the same record in + * our local store). We use it for a {@link TreeSet}, so mainly it's just important that the '0' + * case is correct. Other cases are whatever, just make it something stable. + */ +abstract class DefaultStorageRecordProcessor implements StorageRecordProcessor, Comparator { + + private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class); + private final Set matchedRecords = new TreeSet<>(this); + + /** + * One type of invalid remote data this handles is two records mapping to the same local data. We + * have to trim this bad data out, because if we don't, we'll upload an ID set that only has one + * of the IDs in it, but won't properly delete the dupes, which will then fail our validation + * checks. + *

+ * This is a bit tricky -- as we process records, IDs are written back to the local store, so we + * can't easily be like "oh multiple records are mapping to the same local storage ID". And in + * general we rely on SignalRecords to implement an equals() that includes the StorageId, so using + * a regular set is out. Instead, we use a {@link TreeSet}, which allows us to define a custom + * comparator for checking equality. Then we delegate to the subclass to tell us if two items are + * the same based on their actual data (i.e. two contacts having the same UUID, or two groups + * having the same MasterKey). + */ + @Override + public void process(E remote) throws SQLException { + if (isInvalid(remote)) { + debug(remote.getId(), remote, "Found invalid key! Ignoring it."); + return; + } + + final var local = getMatching(remote); + + if (local.isEmpty()) { + debug(remote.getId(), remote, "No matching local record. Inserting."); + insertLocal(remote); + return; + } + + if (matchedRecords.contains(local.get())) { + debug(remote.getId(), + remote, + "Multiple remote records map to the same local record " + local.get() + "! Ignoring this one."); + return; + } + + matchedRecords.add(local.get()); + + final var merged = merge(remote, local.get()); + if (!merged.equals(remote)) { + debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote)); + } + + if (!merged.equals(local.get())) { + final var update = new StorageRecordUpdate<>(local.get(), merged); + debug(remote.getId(), remote, "[Local Update] " + update); + updateLocal(update); + } + } + + private void debug(StorageId i, E record, String message) { + logger.debug("[" + i + "][" + record.getClass().getSimpleName() + "] " + message); + } + + /** + * @return True if the record is invalid and should be removed from storage service, otherwise false. + */ + protected abstract boolean isInvalid(E remote) throws SQLException; + + /** + * Only records that pass the validity check (i.e. return false from {@link #isInvalid(SignalRecord)}) + * make it to here, so you can assume all records are valid. + */ + protected abstract Optional getMatching(E remote) throws SQLException; + + protected abstract E merge(E remote, E local); + + protected abstract void insertLocal(E record) throws SQLException; + + protected abstract void updateLocal(StorageRecordUpdate update) throws SQLException; +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/GroupV1RecordProcessor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/GroupV1RecordProcessor.java new file mode 100644 index 0000000000000..be95591bda9d8 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/GroupV1RecordProcessor.java @@ -0,0 +1,137 @@ +package org.asamk.signal.manager.syncStorage; + +import org.asamk.signal.manager.api.GroupId; +import org.asamk.signal.manager.api.GroupIdV1; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.util.KeyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Optional; + +/** + * Handles merging remote storage updates into local group v1 state. + */ +public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor { + + private static final Logger logger = LoggerFactory.getLogger(GroupV1RecordProcessor.class); + private final SignalAccount account; + private final Connection connection; + + public GroupV1RecordProcessor(SignalAccount account, Connection connection) { + this.account = account; + this.connection = connection; + } + + /** + * We want to catch: + * - Invalid group IDs + * - GV1 IDs that map to GV2 IDs, meaning we've already migrated them. + */ + @Override + protected boolean isInvalid(SignalGroupV1Record remote) throws SQLException { + try { + final var id = GroupId.unknownVersion(remote.getGroupId()); + if (!(id instanceof GroupIdV1)) { + return true; + } + final var group = account.getGroupStore().getGroup(connection, id); + + if (group instanceof GroupInfoV2) { + logger.debug("We already have an upgraded V2 group for this V1 group -- marking as invalid."); + return true; + } else { + return false; + } + } catch (AssertionError e) { + logger.debug("Bad Group ID -- marking as invalid."); + return true; + } + } + + @Override + protected Optional getMatching(SignalGroupV1Record remote) throws SQLException { + final var id = GroupId.v1(remote.getGroupId()); + final var group = account.getGroupStore().getGroup(connection, id); + + if (group == null) { + return Optional.empty(); + } + + final var storageId = account.getGroupStore().getGroupStorageId(connection, id); + return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV1().get()); + } + + @Override + protected SignalGroupV1Record merge(SignalGroupV1Record remote, SignalGroupV1Record local) { + final var unknownFields = remote.serializeUnknownFields(); + final var blocked = remote.isBlocked(); + final var profileSharing = remote.isProfileSharingEnabled(); + final var archived = remote.isArchived(); + final var forcedUnread = remote.isForcedUnread(); + final var muteUntil = remote.getMuteUntil(); + + final var mergedBuilder = new SignalGroupV1Record.Builder(remote.getId().getRaw(), + remote.getGroupId(), + unknownFields).setBlocked(blocked) + .setProfileSharingEnabled(profileSharing) + .setForcedUnread(forcedUnread) + .setMuteUntil(muteUntil) + .setArchived(archived); + + final var merged = mergedBuilder.build(); + + final var matchesRemote = doProtosMatch(merged, remote); + if (matchesRemote) { + return remote; + } + + final var matchesLocal = doProtosMatch(merged, local); + if (matchesLocal) { + return local; + } + + return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); + } + + @Override + protected void insertLocal(SignalGroupV1Record record) throws SQLException { + // TODO send group info request (after server message queue is empty) + // context.getGroupHelper().sendGroupInfoRequest(groupIdV1, account.getSelfRecipientId()); + StorageRecordUpdate update = new StorageRecordUpdate<>(null, record); + updateLocal(update); + } + + @Override + protected void updateLocal(StorageRecordUpdate update) throws SQLException { + final var groupV1Record = update.newRecord(); + final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId()); + + final var group = account.getGroupStore().getGroup(connection, groupIdV1); + group.setBlocked(groupV1Record.isBlocked()); + account.getGroupStore().updateGroup(connection, group); + account.getGroupStore() + .storeStorageRecord(connection, + group.getGroupId(), + groupV1Record.getId(), + groupV1Record.toProto().encode()); + } + + @Override + public int compare(SignalGroupV1Record lhs, SignalGroupV1Record rhs) { + if (Arrays.equals(lhs.getGroupId(), rhs.getGroupId())) { + return 0; + } else { + return 1; + } + } + + private static boolean doProtosMatch(SignalGroupV1Record merged, SignalGroupV1Record other) { + return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/GroupV2RecordProcessor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/GroupV2RecordProcessor.java new file mode 100644 index 0000000000000..1b1827697ffab --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/GroupV2RecordProcessor.java @@ -0,0 +1,116 @@ +package org.asamk.signal.manager.syncStorage; + +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; +import org.signal.libsignal.zkgroup.groups.GroupMasterKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Optional; + +public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor { + + private static final Logger logger = LoggerFactory.getLogger(GroupV2RecordProcessor.class); + private final SignalAccount account; + private final Connection connection; + + public GroupV2RecordProcessor(SignalAccount account, Connection connection) { + this.account = account; + this.connection = connection; + } + + @Override + protected boolean isInvalid(SignalGroupV2Record remote) { + return remote.getMasterKeyBytes().length != GroupMasterKey.SIZE; + } + + @Override + protected Optional getMatching(SignalGroupV2Record remote) throws SQLException { + final var id = GroupUtils.getGroupIdV2(remote.getMasterKeyOrThrow()); + final var group = account.getGroupStore().getGroup(connection, id); + + if (group == null) { + return Optional.empty(); + } + + final var storageId = account.getGroupStore().getGroupStorageId(connection, id); + return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV2().get()); + } + + @Override + protected SignalGroupV2Record merge(SignalGroupV2Record remote, SignalGroupV2Record local) { + final var unknownFields = remote.serializeUnknownFields(); + final var blocked = remote.isBlocked(); + final var profileSharing = remote.isProfileSharingEnabled(); + final var archived = remote.isArchived(); + final var forcedUnread = remote.isForcedUnread(); + final var muteUntil = remote.getMuteUntil(); + final var notifyForMentionsWhenMuted = remote.notifyForMentionsWhenMuted(); + final var hideStory = remote.shouldHideStory(); + final var storySendMode = remote.getStorySendMode(); + + final var mergedBuilder = new SignalGroupV2Record.Builder(remote.getId().getRaw(), + remote.getMasterKeyBytes(), + unknownFields).setBlocked(blocked) + .setProfileSharingEnabled(profileSharing) + .setArchived(archived) + .setForcedUnread(forcedUnread) + .setMuteUntil(muteUntil) + .setNotifyForMentionsWhenMuted(notifyForMentionsWhenMuted) + .setHideStory(hideStory) + .setStorySendMode(storySendMode); + final var merged = mergedBuilder.build(); + + final var matchesRemote = doProtosMatch(merged, remote); + if (matchesRemote) { + return remote; + } + + final var matchesLocal = doProtosMatch(merged, local); + if (matchesLocal) { + return local; + } + + return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); + } + + @Override + protected void insertLocal(SignalGroupV2Record record) throws SQLException { + StorageRecordUpdate update = new StorageRecordUpdate<>(null, record); + updateLocal(update); + } + + @Override + protected void updateLocal(StorageRecordUpdate update) throws SQLException { + final var groupV2Record = update.newRecord(); + final var groupMasterKey = groupV2Record.getMasterKeyOrThrow(); + + final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey); + group.setBlocked(groupV2Record.isBlocked()); + group.setProfileSharingEnabled(groupV2Record.isProfileSharingEnabled()); + account.getGroupStore().updateGroup(connection, group); + account.getGroupStore() + .storeStorageRecord(connection, + group.getGroupId(), + groupV2Record.getId(), + groupV2Record.toProto().encode()); + } + + @Override + public int compare(SignalGroupV2Record lhs, SignalGroupV2Record rhs) { + if (Arrays.equals(lhs.getMasterKeyBytes(), rhs.getMasterKeyBytes())) { + return 0; + } else { + return 1; + } + } + + private static boolean doProtosMatch(SignalGroupV2Record merged, SignalGroupV2Record other) { + return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessor.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessor.java new file mode 100644 index 0000000000000..45a9956275b45 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessor.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager.syncStorage; + +import org.whispersystems.signalservice.api.storage.SignalRecord; + +import java.sql.SQLException; + +/** + * Handles processing a remote record, which involves applying any local changes that need to be + * made based on the remote records. + */ +interface StorageRecordProcessor { + + void process(E remoteRecord) throws SQLException; +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageRecordUpdate.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageRecordUpdate.java new file mode 100644 index 0000000000000..8dcb7b3405374 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageRecordUpdate.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager.syncStorage; + +import org.whispersystems.signalservice.api.storage.SignalRecord; + +/** + * Represents a pair of records: one old, and one new. The new record should replace the old. + */ +record StorageRecordUpdate(E oldRecord, E newRecord) { + + @Override + public String toString() { + return newRecord.describeDiff(oldRecord); + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java new file mode 100644 index 0000000000000..8cf0604833dce --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java @@ -0,0 +1,154 @@ +package org.asamk.signal.manager.syncStorage; + +import org.asamk.signal.manager.api.PhoneNumberSharingMode; +import org.asamk.signal.manager.api.TrustLevel; +import org.asamk.signal.manager.storage.configuration.ConfigurationStore; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.storage.recipients.Recipient; +import org.whispersystems.signalservice.api.push.UsernameLinkComponents; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.api.storage.SignalContactRecord; +import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; +import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink; +import org.whispersystems.signalservice.internal.storage.protos.ContactRecord; +import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; + +import java.util.Optional; + +import okio.ByteString; + +public final class StorageSyncModels { + + private StorageSyncModels() { + } + + public static AccountRecord.PhoneNumberSharingMode localToRemote(PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) { + return switch (phoneNumberPhoneNumberSharingMode) { + case EVERYBODY -> AccountRecord.PhoneNumberSharingMode.EVERYBODY; + case CONTACTS, NOBODY -> AccountRecord.PhoneNumberSharingMode.NOBODY; + }; + } + + public static PhoneNumberSharingMode remoteToLocal(AccountRecord.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) { + return switch (phoneNumberPhoneNumberSharingMode) { + case EVERYBODY -> PhoneNumberSharingMode.EVERYBODY; + case UNKNOWN, NOBODY -> PhoneNumberSharingMode.NOBODY; + }; + } + + public static SignalStorageRecord localToRemoteRecord( + ConfigurationStore configStore, + Recipient self, + UsernameLinkComponents usernameLinkComponents, + byte[] rawStorageId + ) { + final var builder = new SignalAccountRecord.Builder(rawStorageId, self.getStorageRecord()); + if (self.getProfileKey() != null) { + builder.setProfileKey(self.getProfileKey().serialize()); + } + if (self.getProfile() != null) { + builder.setGivenName(self.getProfile().getGivenName()) + .setFamilyName(self.getProfile().getFamilyName()) + .setAvatarUrlPath(self.getProfile().getAvatarUrlPath()); + } + builder.setTypingIndicatorsEnabled(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true)) + .setReadReceiptsEnabled(Optional.ofNullable(configStore.getReadReceipts()).orElse(true)) + .setSealedSenderIndicatorsEnabled(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators()) + .orElse(true)) + .setLinkPreviewsEnabled(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true)) + .setUnlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false)) + .setPhoneNumberSharingMode(localToRemote(Optional.ofNullable(configStore.getPhoneNumberSharingMode()) + .orElse(PhoneNumberSharingMode.EVERYBODY))) + .setE164(self.getAddress().number().orElse("")) + .setUsername(self.getAddress().username().orElse(null)); + if (usernameLinkComponents != null) { + final var linkColor = configStore.getUsernameLinkColor(); + builder.setUsernameLink(new UsernameLink.Builder().entropy(ByteString.of(usernameLinkComponents.getEntropy())) + .serverId(UuidUtil.toByteString(usernameLinkComponents.getServerId())) + .color(linkColor == null ? UsernameLink.Color.UNKNOWN : UsernameLink.Color.valueOf(linkColor)) + .build()); + } + + return SignalStorageRecord.forAccount(builder.build()); + } + + public static SignalStorageRecord localToRemoteRecord( + Recipient recipient, IdentityInfo identity, byte[] rawStorageId + ) { + final var address = recipient.getAddress(); + final var builder = new SignalContactRecord.Builder(rawStorageId, + address.aci().orElse(null), + recipient.getStorageRecord()).setE164(address.number().orElse(null)) + .setPni(address.pni().orElse(null)) + .setUsername(address.username().orElse(null)) + .setProfileKey(recipient.getProfileKey() == null ? null : recipient.getProfileKey().serialize()); + if (recipient.getProfile() != null) { + builder.setProfileGivenName(recipient.getProfile().getGivenName()) + .setProfileFamilyName(recipient.getProfile().getFamilyName()); + } + if (recipient.getContact() != null) { + builder.setSystemGivenName(recipient.getContact().givenName()) + .setSystemFamilyName(recipient.getContact().familyName()) + .setSystemNickname(recipient.getContact().nickName()) + .setBlocked(recipient.getContact().isBlocked()) + .setProfileSharingEnabled(recipient.getContact().isProfileSharingEnabled()) + .setMuteUntil(recipient.getContact().muteUntil()) + .setHideStory(recipient.getContact().hideStory()) + .setUnregisteredTimestamp(recipient.getContact().unregisteredTimestamp() == null + ? 0 + : recipient.getContact().unregisteredTimestamp()) + .setArchived(recipient.getContact().isArchived()) + .setHidden(recipient.getContact().isHidden()); + } + if (identity != null) { + builder.setIdentityKey(identity.getIdentityKey().serialize()) + .setIdentityState(localToRemote(identity.getTrustLevel())); + } + return SignalStorageRecord.forContact(builder.build()); + } + + public static SignalStorageRecord localToRemoteRecord( + GroupInfoV1 group, byte[] rawStorageId + ) { + final var builder = new SignalGroupV1Record.Builder(rawStorageId, + group.getGroupId().serialize(), + group.getStorageRecord()); + builder.setBlocked(group.isBlocked()); + builder.setArchived(group.archived); + builder.setProfileSharingEnabled(true); + return SignalStorageRecord.forGroupV1(builder.build()); + } + + public static SignalStorageRecord localToRemoteRecord( + GroupInfoV2 group, byte[] rawStorageId + ) { + final var builder = new SignalGroupV2Record.Builder(rawStorageId, + group.getMasterKey(), + group.getStorageRecord()); + builder.setBlocked(group.isBlocked()); + builder.setProfileSharingEnabled(group.isProfileSharingEnabled()); + return SignalStorageRecord.forGroupV2(builder.build()); + } + + public static TrustLevel remoteToLocal(ContactRecord.IdentityState identityState) { + return switch (identityState) { + case DEFAULT -> TrustLevel.TRUSTED_UNVERIFIED; + case UNVERIFIED -> TrustLevel.UNTRUSTED; + case VERIFIED -> TrustLevel.TRUSTED_VERIFIED; + }; + } + + private static IdentityState localToRemote(TrustLevel local) { + return switch (local) { + case TRUSTED_VERIFIED -> IdentityState.VERIFIED; + case UNTRUSTED -> IdentityState.UNVERIFIED; + default -> IdentityState.DEFAULT; + }; + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java new file mode 100644 index 0000000000000..e69481edc4133 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java @@ -0,0 +1,237 @@ +package org.asamk.signal.manager.syncStorage; + +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.signal.core.util.Base64; +import org.signal.core.util.SetUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.storage.SignalStorageManifest; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; + +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public final class StorageSyncValidations { + + private static final Logger logger = LoggerFactory.getLogger(StorageSyncValidations.class); + + private StorageSyncValidations() { + } + + public static void validate( + WriteOperationResult result, + SignalStorageManifest previousManifest, + boolean forcePushPending, + RecipientAddress self + ) { + validateManifestAndInserts(result.manifest(), result.inserts(), self); + + if (!result.deletes().isEmpty()) { + Set allSetEncoded = result.manifest() + .getStorageIds() + .stream() + .map(StorageId::getRaw) + .map(Base64::encodeWithPadding) + .collect(Collectors.toSet()); + + for (byte[] delete : result.deletes()) { + String encoded = Base64.encodeWithPadding(delete); + if (allSetEncoded.contains(encoded)) { + throw new DeletePresentInFullIdSetError(); + } + } + } + + if (previousManifest.getVersion() == 0) { + logger.debug( + "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests."); + return; + } + + if (result.manifest().getVersion() != previousManifest.getVersion() + 1) { + throw new IncorrectManifestVersionError(); + } + + if (forcePushPending) { + logger.debug( + "Force push pending, not bothering with additional validations around the diffs between the two manifests."); + return; + } + + Set previousIds = previousManifest.getStorageIds() + .stream() + .map(id -> ByteBuffer.wrap(id.getRaw())) + .collect(Collectors.toSet()); + Set newIds = result.manifest() + .getStorageIds() + .stream() + .map(id -> ByteBuffer.wrap(id.getRaw())) + .collect(Collectors.toSet()); + + Set manifestInserts = SetUtil.difference(newIds, previousIds); + Set manifestDeletes = SetUtil.difference(previousIds, newIds); + + Set declaredInserts = result.inserts() + .stream() + .map(r -> ByteBuffer.wrap(r.getId().getRaw())) + .collect(Collectors.toSet()); + Set declaredDeletes = result.deletes().stream().map(ByteBuffer::wrap).collect(Collectors.toSet()); + + if (declaredInserts.size() > manifestInserts.size()) { + logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size()); + throw new MoreInsertsThanExpectedError(); + } + + if (declaredInserts.size() < manifestInserts.size()) { + logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size()); + throw new LessInsertsThanExpectedError(); + } + + if (!declaredInserts.containsAll(manifestInserts)) { + throw new InsertMismatchError(); + } + + if (declaredDeletes.size() > manifestDeletes.size()) { + logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size()); + throw new MoreDeletesThanExpectedError(); + } + + if (declaredDeletes.size() < manifestDeletes.size()) { + logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size()); + throw new LessDeletesThanExpectedError(); + } + + if (!declaredDeletes.containsAll(manifestDeletes)) { + throw new DeleteMismatchError(); + } + } + + public static void validateForcePush( + SignalStorageManifest manifest, List inserts, RecipientAddress self + ) { + validateManifestAndInserts(manifest, inserts, self); + } + + private static void validateManifestAndInserts( + SignalStorageManifest manifest, List inserts, RecipientAddress self + ) { + int accountCount = 0; + for (StorageId id : manifest.getStorageIds()) { + accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue() ? 1 : 0; + } + + if (accountCount > 1) { + throw new MultipleAccountError(); + } + + if (accountCount == 0) { + throw new MissingAccountError(); + } + + Set allSet = new HashSet<>(manifest.getStorageIds()); + Set insertSet = inserts.stream().map(SignalStorageRecord::getId).collect(Collectors.toSet()); + Set rawIdSet = allSet.stream().map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet()); + + if (allSet.size() != manifest.getStorageIds().size()) { + throw new DuplicateStorageIdError(); + } + + if (rawIdSet.size() != allSet.size()) { + List ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.CONTACT.getValue()); + if (ids.size() != new HashSet<>(ids).size()) { + throw new DuplicateContactIdError(); + } + + ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.GROUPV1.getValue()); + if (ids.size() != new HashSet<>(ids).size()) { + throw new DuplicateGroupV1IdError(); + } + + ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.GROUPV2.getValue()); + if (ids.size() != new HashSet<>(ids).size()) { + throw new DuplicateGroupV2IdError(); + } + + ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST.getValue()); + if (ids.size() != new HashSet<>(ids).size()) { + throw new DuplicateDistributionListIdError(); + } + + throw new DuplicateRawIdAcrossTypesError(); + } + + if (inserts.size() > insertSet.size()) { + throw new DuplicateInsertInWriteError(); + } + + for (SignalStorageRecord insert : inserts) { + if (!allSet.contains(insert.getId())) { + throw new InsertNotPresentInFullIdSetError(); + } + + if (insert.isUnknown()) { + throw new UnknownInsertError(); + } + + if (insert.getContact().isPresent()) { + final var contact = insert.getContact().get(); + final var aci = contact.getAci(); + final var pni = contact.getPni(); + final var number = contact.getNumber(); + final var username = contact.getUsername(); + final var address = new RecipientAddress(aci, pni, number, username); + if (self.matches(address)) { + throw new SelfAddedAsContactError(); + } + } + if (insert.getAccount().isPresent() && insert.getAccount().get().getProfileKey().isEmpty()) { + logger.debug("Uploading a null profile key in our AccountRecord!"); + } + } + } + + private static final class DuplicateStorageIdError extends Error {} + + private static final class DuplicateRawIdAcrossTypesError extends Error {} + + private static final class DuplicateContactIdError extends Error {} + + private static final class DuplicateGroupV1IdError extends Error {} + + private static final class DuplicateGroupV2IdError extends Error {} + + private static final class DuplicateDistributionListIdError extends Error {} + + private static final class DuplicateInsertInWriteError extends Error {} + + private static final class InsertNotPresentInFullIdSetError extends Error {} + + private static final class DeletePresentInFullIdSetError extends Error {} + + private static final class UnknownInsertError extends Error {} + + private static final class MultipleAccountError extends Error {} + + private static final class MissingAccountError extends Error {} + + private static final class SelfAddedAsContactError extends Error {} + + private static final class IncorrectManifestVersionError extends Error {} + + private static final class MoreInsertsThanExpectedError extends Error {} + + private static final class LessInsertsThanExpectedError extends Error {} + + private static final class InsertMismatchError extends Error {} + + private static final class MoreDeletesThanExpectedError extends Error {} + + private static final class LessDeletesThanExpectedError extends Error {} + + private static final class DeleteMismatchError extends Error {} +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/WriteOperationResult.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/WriteOperationResult.java new file mode 100644 index 0000000000000..97e3579a7bf50 --- /dev/null +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/syncStorage/WriteOperationResult.java @@ -0,0 +1,30 @@ +package org.asamk.signal.manager.syncStorage; + +import org.whispersystems.signalservice.api.storage.SignalStorageManifest; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; + +import java.util.List; +import java.util.Locale; + +public record WriteOperationResult( + SignalStorageManifest manifest, List inserts, List deletes +) { + + public boolean isEmpty() { + return inserts.isEmpty() && deletes.isEmpty(); + } + + @Override + public String toString() { + if (isEmpty()) { + return "Empty"; + } else { + return String.format(Locale.ROOT, + "ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d", + manifest.getVersion(), + manifest.getStorageIds().size(), + inserts.size(), + deletes.size()); + } + } +} diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/KeyUtils.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/KeyUtils.java index 486e3655bf0cf..bfcb750c680f5 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/KeyUtils.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/KeyUtils.java @@ -113,6 +113,10 @@ public static MasterKey createMasterKey() { return MasterKey.createNew(secureRandom); } + public static byte[] createRawStorageId() { + return getSecretBytes(16); + } + private static String getSecret(int size) { var secret = getSecretBytes(size); return Base64.getEncoder().encodeToString(secret); diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/MessageCacheUtils.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/MessageCacheUtils.java index b3c9457baed7e..316c349e36054 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/MessageCacheUtils.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/MessageCacheUtils.java @@ -18,9 +18,9 @@ public class MessageCacheUtils { - private final static Logger logger = LoggerFactory.getLogger(MessageCacheUtils.class); + private static final Logger logger = LoggerFactory.getLogger(MessageCacheUtils.class); - final static int CURRENT_VERSION = 9; + static final int CURRENT_VERSION = 9; public static SignalServiceEnvelope loadEnvelope(File file) throws IOException { try (var f = new FileInputStream(file)) { diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/NumberVerificationUtils.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/NumberVerificationUtils.java index 62474e665460d..a5c91f5b3b855 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/NumberVerificationUtils.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/NumberVerificationUtils.java @@ -26,7 +26,7 @@ public class NumberVerificationUtils { - private final static Logger logger = LoggerFactory.getLogger(NumberVerificationUtils.class); + private static final Logger logger = LoggerFactory.getLogger(NumberVerificationUtils.class); public static String handleVerificationSession( SignalServiceAccountManager accountManager, diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/PaymentUtils.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/PaymentUtils.java index ca55008c93ee5..fe338e73da470 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/PaymentUtils.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/PaymentUtils.java @@ -12,7 +12,7 @@ public class PaymentUtils { - private final static Logger logger = LoggerFactory.getLogger(PaymentUtils.class); + private static final Logger logger = LoggerFactory.getLogger(PaymentUtils.class); private PaymentUtils() { } diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/ProfileUtils.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/ProfileUtils.java index 40fc8f7b2c287..85c9fdd60c17a 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -19,7 +19,7 @@ public class ProfileUtils { - private final static Logger logger = LoggerFactory.getLogger(ProfileUtils.class); + private static final Logger logger = LoggerFactory.getLogger(ProfileUtils.class); public static Profile decryptProfile( final ProfileKey profileKey, final SignalServiceProfile encryptedProfile diff --git a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/Utils.java b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/Utils.java index 6e9c701e13cdd..e6d8e4d3097b1 100644 --- a/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/Utils.java +++ b/bundles/org.openhab.binding.signal/src/3rdparty/java/org/asamk/signal/manager/util/Utils.java @@ -32,7 +32,7 @@ public class Utils { - private final static Logger logger = LoggerFactory.getLogger(Utils.class); + private static final Logger logger = LoggerFactory.getLogger(Utils.class); public static Pair> createStreamDetailsFromDataURI(final String dataURI) { final DataURI uri = DataURI.of(dataURI); diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBindingConstants.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBindingConstants.java index d74608f18a4ee..ef12fcdb6f77e 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBindingConstants.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBindingConstants.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBridgeConfiguration.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBridgeConfiguration.java index 59c7cb54bceb0..2a2e7394ce5c6 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBridgeConfiguration.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalBridgeConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationConfiguration.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationConfiguration.java index b5a82917abfe3..1e51cc95eb721 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationConfiguration.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationDiscoveryService.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationDiscoveryService.java index 59be11be67b1d..0f5c5447d9f0c 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationDiscoveryService.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalConversationDiscoveryService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalHandlerFactory.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalHandlerFactory.java index 4c083c489a950..1114d5c60fc09 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalHandlerFactory.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/SignalHandlerFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/actions/SignalActions.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/actions/SignalActions.java index 43df816182f55..26be417206c2c 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/actions/SignalActions.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/actions/SignalActions.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalBridgeHandler.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalBridgeHandler.java index c62279654f1d9..8db6f71abe9f3 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalBridgeHandler.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalBridgeHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalConversationHandler.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalConversationHandler.java index 4f9b9ab8ba549..00fd1182d11cd 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalConversationHandler.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/handler/SignalConversationHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/package-info.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/package-info.java index 6d3272b533e76..6f5ad73515f17 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/package-info.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/package-info.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentCreationException.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentCreationException.java index 00663d519017b..6d90854d7b123 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentCreationException.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentCreationException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentUtils.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentUtils.java index a6999efc450eb..ef1087519bf65 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentUtils.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/AttachmentUtils.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryReport.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryReport.java index ff06c24ab2f8a..ef77c15207c10 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryReport.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryReport.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryStatus.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryStatus.java index 743eaa96613b9..c1dd66eeadc12 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryStatus.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/DeliveryStatus.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/IncompleteRegistrationException.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/IncompleteRegistrationException.java index 79916e88e134a..a01558a9f09e0 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/IncompleteRegistrationException.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/IncompleteRegistrationException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/MessageListener.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/MessageListener.java index 246c184fb8373..63becfe6a2a6a 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/MessageListener.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/MessageListener.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/ProvisionType.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/ProvisionType.java index 5c6afebb4bef0..41c4ee1e86217 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/ProvisionType.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/ProvisionType.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationState.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationState.java index 55aeef2785805..3f70b08e073e5 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationState.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationState.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationType.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationType.java index b1fd1eb2a5667..2ed893f47bfc4 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationType.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/RegistrationType.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/SignalService.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/SignalService.java index 93f44c14c8598..0016b4f356c82 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/SignalService.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/SignalService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. @@ -279,12 +279,15 @@ public DeliveryReport send(String address, String message, @Nullable String atta return new DeliveryReport(DeliveryStatus.FAILED, address); } + boolean notify; RecipientIdentifier recipient; if (NOTE_TO_SELF.equalsIgnoreCase(address.trim())) { recipient = RecipientIdentifier.NoteToSelf.INSTANCE; + notify = false; } else { try { recipient = RecipientIdentifier.Single.fromString(address, address); + notify = true; } catch (InvalidNumberException e) { logger.warn("Cannot send message to {}, cause {}", address, e.getMessage()); return new DeliveryReport(DeliveryStatus.FAILED, address); @@ -293,11 +296,10 @@ public DeliveryReport send(String address, String message, @Nullable String atta SendMessageResults sendResults; try { List attachments = attachment == null ? List.of() : List.of(attachment); - sendResults = managerFinal - .sendMessage( - new Message(message, attachments, Collections.emptyList(), Optional.empty(), - Optional.empty(), Collections.emptyList(), Optional.empty(), List.of()), - Collections.singleton(recipient)); + sendResults = managerFinal.sendMessage( + new Message(message, attachments, Collections.emptyList(), Optional.empty(), Optional.empty(), + Collections.emptyList(), Optional.empty(), List.of()), + Collections.singleton(recipient), notify); } catch (IOException | AttachmentInvalidException | NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException | UnregisteredRecipientException | InvalidStickerException e) { logger.warn("Cannot send message to {}, cause {}", address, e.getMessage()); diff --git a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/StateListener.java b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/StateListener.java index 8d769ef55599e..db032190fefc6 100644 --- a/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/StateListener.java +++ b/bundles/org.openhab.binding.signal/src/main/java/org/openhab/binding/signal/internal/protocol/StateListener.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2023 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/aarch64/libsignal_jni.dylib b/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/aarch64/libsignal_jni.dylib index 5e0fc74cdd1b5..e83acce5468bd 100644 Binary files a/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/aarch64/libsignal_jni.dylib and b/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/aarch64/libsignal_jni.dylib differ diff --git a/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/amd64/libsignal_jni.dylib b/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/amd64/libsignal_jni.dylib index 31808dfa2e69a..8309ea4b5e29c 100644 Binary files a/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/amd64/libsignal_jni.dylib and b/bundles/org.openhab.binding.signal/src/main/resources/lib/apple/amd64/libsignal_jni.dylib differ diff --git a/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/arm64/libsignal_jni.so b/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/arm64/libsignal_jni.so index 9dcd47b5efc52..abed197fc3779 100644 Binary files a/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/arm64/libsignal_jni.so and b/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/arm64/libsignal_jni.so differ diff --git a/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/armv7/libsignal_jni.so b/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/armv7/libsignal_jni.so index 09737db6a9ff5..2603a3f037084 100644 Binary files a/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/armv7/libsignal_jni.so and b/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/armv7/libsignal_jni.so differ diff --git a/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/x86-64/libsignal_jni.so b/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/x86-64/libsignal_jni.so index 6874d03b42232..d9daff8becd47 100644 Binary files a/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/x86-64/libsignal_jni.so and b/bundles/org.openhab.binding.signal/src/main/resources/lib/linux/x86-64/libsignal_jni.so differ diff --git a/bundles/org.openhab.binding.signal/src/main/resources/lib/windows/x86-64/signal_jni.dll b/bundles/org.openhab.binding.signal/src/main/resources/lib/windows/x86-64/signal_jni.dll index 637ce46031eef..52f97d6ba73f9 100644 Binary files a/bundles/org.openhab.binding.signal/src/main/resources/lib/windows/x86-64/signal_jni.dll and b/bundles/org.openhab.binding.signal/src/main/resources/lib/windows/x86-64/signal_jni.dll differ