package org.asamk.signal.manager.helper;
+import org.asamk.signal.manager.api.PhoneNumberSharingMode;
+import org.asamk.signal.manager.api.Profile;
+import org.asamk.signal.manager.internal.SignalDependencies;
+import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.jetbrains.annotations.Nullable;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
-import org.whispersystems.libsignal.util.guava.Optional;
+import org.signal.libsignal.metadata.certificate.SenderCertificate;
+import org.signal.libsignal.zkgroup.profiles.ProfileKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
-import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
-import java.util.Collection;
+import java.io.IOException;
import java.util.List;
-import java.util.stream.Collectors;
+import java.util.concurrent.TimeUnit;
-import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes;
+import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class UnidentifiedAccessHelper {
- private final SelfProfileKeyProvider selfProfileKeyProvider;
+ 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 ProfileKeyProvider profileKeyProvider;
+ private final SignalAccount account;
+ private final SignalDependencies dependencies;
+ private final Context context;
- private final ProfileProvider profileProvider;
+ private SenderCertificate privacySenderCertificate;
+ private SenderCertificate senderCertificate;
- private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider;
+ public UnidentifiedAccessHelper(final Context context) {
+ this.account = context.getAccount();
+ this.dependencies = context.getDependencies();
+ this.context = context;
+ }
- public UnidentifiedAccessHelper(
- final SelfProfileKeyProvider selfProfileKeyProvider,
- final ProfileKeyProvider profileKeyProvider,
- final ProfileProvider profileProvider,
- final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider
- ) {
- this.selfProfileKeyProvider = selfProfileKeyProvider;
- this.profileKeyProvider = profileKeyProvider;
- this.profileProvider = profileProvider;
- this.senderCertificateProvider = senderCertificateProvider;
+ public void rotateSenderCertificates() {
+ privacySenderCertificate = null;
+ senderCertificate = null;
}
- private byte[] getSelfUnidentifiedAccessKey() {
- return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
+ public List<SealedSenderAccess> getSealedSenderAccessFor(List<RecipientId> recipients) {
+ return recipients.stream().map(this::getAccessFor).map(SealedSenderAccess::forIndividual).toList();
}
- public byte[] getTargetUnidentifiedAccessKey(RecipientId recipient) {
- var targetProfile = profileProvider.getProfile(recipient);
- if (targetProfile == null) {
+ public @Nullable SealedSenderAccess getSealedSenderAccessFor(RecipientId recipient) {
+ return getSealedSenderAccessFor(recipient, false);
+ }
+
+ public @Nullable SealedSenderAccess getSealedSenderAccessFor(RecipientId recipient, boolean noRefresh) {
+ return SealedSenderAccess.forIndividual(getAccessFor(recipient, noRefresh));
+ }
+
+ public List<UnidentifiedAccess> getAccessFor(List<RecipientId> recipients) {
+ return recipients.stream().map(this::getAccessFor).toList();
+ }
+
+ private @Nullable UnidentifiedAccess getAccessFor(RecipientId recipient) {
+ return getAccessFor(recipient, false);
+ }
+
+ private @Nullable UnidentifiedAccess getAccessFor(RecipientId recipientId, boolean noRefresh) {
+ var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientId, noRefresh);
+ if (recipientUnidentifiedAccessKey == null) {
+ logger.trace("Unidentified access not available for {}", recipientId);
return null;
}
- switch (targetProfile.getUnidentifiedAccessMode()) {
- case ENABLED:
- var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
- if (theirProfileKey == null) {
- return null;
- }
-
- return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
- case UNRESTRICTED:
- return createUnrestrictedUnidentifiedAccess();
- default:
- return null;
+ var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(noRefresh);
+ if (selfUnidentifiedAccessKey == null) {
+ logger.trace("Unidentified access not available for self");
+ return null;
}
- }
- public Optional<UnidentifiedAccessPair> getAccessForSync() {
- var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
-
- if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
- return Optional.absent();
+ var senderCertificate = getSenderCertificateFor(recipientId);
+ if (senderCertificate == null) {
+ logger.trace("Unidentified access not available due to missing sender certificate");
+ return null;
}
try {
- return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(selfUnidentifiedAccessKey,
- selfUnidentifiedAccessCertificate),
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)));
+ return new UnidentifiedAccess(recipientUnidentifiedAccessKey, senderCertificate, false);
} catch (InvalidCertificateException e) {
- return Optional.absent();
+ return null;
}
}
- public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) {
- return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
+ private byte[] getSenderCertificateFor(final RecipientId recipientId) {
+ final var sharingMode = account.getConfigurationStore().getPhoneNumberSharingMode();
+ if (sharingMode == PhoneNumberSharingMode.EVERYBODY || (
+ sharingMode == PhoneNumberSharingMode.CONTACTS
+ && account.getContactStore().getContact(recipientId) != null
+ )) {
+ logger.trace("Using normal sender certificate for message to {}", recipientId);
+ return getSenderCertificate();
+ } else {
+ logger.trace("Using phone number privacy sender certificate for message to {}", recipientId);
+ return getSenderCertificateForPhoneNumberPrivacy();
+ }
}
- public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
- var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
- var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
-
- if (recipientUnidentifiedAccessKey == null
- || selfUnidentifiedAccessKey == null
- || selfUnidentifiedAccessCertificate == null) {
- return Optional.absent();
+ private byte[] getSenderCertificateForPhoneNumberPrivacy() {
+ if (privacySenderCertificate != null && System.currentTimeMillis() < (
+ privacySenderCertificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
+ )) {
+ return privacySenderCertificate.getSerialized();
}
+ try {
+ final var certificate = handleResponseException(dependencies.getCertificateApi()
+ .getSenderCertificateForPhoneNumberPrivacy());
+ privacySenderCertificate = new SenderCertificate(certificate);
+ return certificate;
+ } catch (IOException | InvalidCertificateException e) {
+ logger.warn("Failed to get sender certificate (pnp), ignoring: {}", e.getMessage());
+ return null;
+ }
+ }
+ private byte[] getSenderCertificate() {
+ if (senderCertificate != null && System.currentTimeMillis() < (
+ senderCertificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER
+ )) {
+ return senderCertificate.getSerialized();
+ }
try {
- return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(recipientUnidentifiedAccessKey,
- selfUnidentifiedAccessCertificate),
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)));
- } catch (InvalidCertificateException e) {
- return Optional.absent();
+ final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate());
+ this.senderCertificate = new SenderCertificate(certificate);
+ return certificate;
+ } catch (IOException | InvalidCertificateException e) {
+ logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
+ return null;
+ }
+ }
+
+ private byte[] getSelfUnidentifiedAccessKey(boolean noRefresh) {
+ var selfProfile = noRefresh
+ ? account.getProfileStore().getProfile(account.getSelfRecipientId())
+ : context.getProfileHelper().getSelfProfile();
+ if (selfProfile != null
+ && selfProfile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED) {
+ return createUnrestrictedUnidentifiedAccess();
}
+ return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
+ }
+
+ private byte[] getTargetUnidentifiedAccessKey(RecipientId recipientId, boolean noRefresh) {
+ var targetProfile = noRefresh
+ ? account.getProfileStore().getProfile(recipientId)
+ : context.getProfileHelper().getRecipientProfile(recipientId);
+ if (targetProfile == null) {
+ return null;
+ }
+
+ var theirProfileKey = account.getProfileStore().getProfileKey(recipientId);
+ return getTargetUnidentifiedAccessKey(targetProfile, theirProfileKey);
+ }
+
+ private static byte[] getTargetUnidentifiedAccessKey(
+ final Profile targetProfile,
+ final ProfileKey theirProfileKey
+ ) {
+ return switch (targetProfile.getUnidentifiedAccessMode()) {
+ case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
+ case UNRESTRICTED -> createUnrestrictedUnidentifiedAccess();
+ default -> null;
+ };
}
private static byte[] createUnrestrictedUnidentifiedAccess() {
- return getSecretBytes(16);
+ return UNRESTRICTED_KEY;
}
}