mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20:39 +00:00
Implement support for receiving sender key messages
This commit is contained in:
parent
35622ac684
commit
ac18006abb
9 changed files with 664 additions and 13 deletions
|
@ -34,12 +34,7 @@ public class ServiceConfig {
|
|||
} catch (Throwable ignored) {
|
||||
zkGroupAvailable = false;
|
||||
}
|
||||
capabilities = new AccountAttributes.Capabilities(false,
|
||||
zkGroupAvailable,
|
||||
false,
|
||||
zkGroupAvailable,
|
||||
false,
|
||||
true);
|
||||
capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true);
|
||||
|
||||
try {
|
||||
TrustStore contactTrustStore = new IasTrustStore();
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.signal.zkgroup.InvalidInputException;
|
|||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
@ -173,10 +174,20 @@ public final class IncomingMessageHandler {
|
|||
) {
|
||||
var actions = new ArrayList<HandleAction>();
|
||||
final RecipientId sender;
|
||||
final int senderDeviceId;
|
||||
if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
|
||||
sender = recipientResolver.resolveRecipient(envelope.getSourceAddress());
|
||||
senderDeviceId = envelope.getSourceDevice();
|
||||
} else {
|
||||
sender = recipientResolver.resolveRecipient(content.getSender());
|
||||
senderDeviceId = content.getSenderDevice();
|
||||
}
|
||||
|
||||
if (content.getSenderKeyDistributionMessage().isPresent()) {
|
||||
final var message = content.getSenderKeyDistributionMessage().get();
|
||||
final var protocolAddress = new SignalProtocolAddress(addressResolver.resolveSignalServiceAddress(sender)
|
||||
.getIdentifier(), senderDeviceId);
|
||||
dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message);
|
||||
}
|
||||
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
||||
public interface RecipientAddressResolver {
|
||||
|
||||
RecipientAddress resolveRecipientAddress(RecipientId recipientId);
|
||||
}
|
|
@ -24,6 +24,7 @@ import org.asamk.signal.manager.storage.recipients.Profile;
|
|||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
||||
import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore;
|
||||
import org.asamk.signal.manager.storage.sessions.SessionStore;
|
||||
import org.asamk.signal.manager.storage.stickers.StickerStore;
|
||||
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
|
||||
|
@ -95,6 +96,7 @@ public class SignalAccount implements Closeable {
|
|||
private SignedPreKeyStore signedPreKeyStore;
|
||||
private SessionStore sessionStore;
|
||||
private IdentityKeyStore identityKeyStore;
|
||||
private SenderKeyStore senderKeyStore;
|
||||
private GroupStore groupStore;
|
||||
private GroupStore.Storage groupStoreStorage;
|
||||
private RecipientStore recipientStore;
|
||||
|
@ -181,10 +183,15 @@ public class SignalAccount implements Closeable {
|
|||
identityKey,
|
||||
registrationId,
|
||||
trustNewIdentity);
|
||||
senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, username),
|
||||
getSenderKeysPath(dataPath, username),
|
||||
recipientStore::resolveRecipientAddress,
|
||||
recipientStore);
|
||||
signalProtocolStore = new SignalProtocolStore(preKeyStore,
|
||||
signedPreKeyStore,
|
||||
sessionStore,
|
||||
identityKeyStore,
|
||||
senderKeyStore,
|
||||
this::isMultiDevice);
|
||||
|
||||
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||
|
@ -221,6 +228,7 @@ public class SignalAccount implements Closeable {
|
|||
account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
|
||||
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
|
||||
account.sessionStore.archiveAllSessions();
|
||||
account.senderKeyStore.deleteAll();
|
||||
account.clearAllPreKeys();
|
||||
return account;
|
||||
}
|
||||
|
@ -303,6 +311,7 @@ public class SignalAccount implements Closeable {
|
|||
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
senderKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
}
|
||||
|
||||
public static File getFileName(File dataPath, String username) {
|
||||
|
@ -343,6 +352,14 @@ public class SignalAccount implements Closeable {
|
|||
return new File(getUserPath(dataPath, username), "sessions");
|
||||
}
|
||||
|
||||
private static File getSenderKeysPath(File dataPath, String username) {
|
||||
return new File(getUserPath(dataPath, username), "sender-keys");
|
||||
}
|
||||
|
||||
private static File getSharedSenderKeysFile(File dataPath, String username) {
|
||||
return new File(getUserPath(dataPath, username), "shared-sender-keys-store");
|
||||
}
|
||||
|
||||
private static File getRecipientsStoreFile(File dataPath, String username) {
|
||||
return new File(getUserPath(dataPath, username), "recipients-store");
|
||||
}
|
||||
|
@ -768,6 +785,10 @@ public class SignalAccount implements Closeable {
|
|||
return stickerStore;
|
||||
}
|
||||
|
||||
public SenderKeyStore getSenderKeyStore() {
|
||||
return senderKeyStore;
|
||||
}
|
||||
|
||||
public MessageCache getMessageCache() {
|
||||
return messageCache;
|
||||
}
|
||||
|
@ -932,6 +953,7 @@ public class SignalAccount implements Closeable {
|
|||
save();
|
||||
|
||||
getSessionStore().archiveAllSessions();
|
||||
senderKeyStore.deleteAll();
|
||||
final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
|
||||
final var publicKey = getIdentityKeyPair().getPublicKey();
|
||||
getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.whispersystems.libsignal.state.SessionRecord;
|
|||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceSessionStore;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
|
||||
|
@ -28,6 +29,7 @@ public class SignalProtocolStore implements SignalServiceDataStore {
|
|||
private final SignedPreKeyStore signedPreKeyStore;
|
||||
private final SignalServiceSessionStore sessionStore;
|
||||
private final IdentityKeyStore identityKeyStore;
|
||||
private final SignalServiceSenderKeyStore senderKeyStore;
|
||||
private final Supplier<Boolean> isMultiDevice;
|
||||
|
||||
public SignalProtocolStore(
|
||||
|
@ -35,12 +37,14 @@ public class SignalProtocolStore implements SignalServiceDataStore {
|
|||
final SignedPreKeyStore signedPreKeyStore,
|
||||
final SignalServiceSessionStore sessionStore,
|
||||
final IdentityKeyStore identityKeyStore,
|
||||
final SignalServiceSenderKeyStore senderKeyStore,
|
||||
final Supplier<Boolean> isMultiDevice
|
||||
) {
|
||||
this.preKeyStore = preKeyStore;
|
||||
this.signedPreKeyStore = signedPreKeyStore;
|
||||
this.sessionStore = sessionStore;
|
||||
this.identityKeyStore = identityKeyStore;
|
||||
this.senderKeyStore = senderKeyStore;
|
||||
this.isMultiDevice = isMultiDevice;
|
||||
}
|
||||
|
||||
|
@ -163,31 +167,29 @@ public class SignalProtocolStore implements SignalServiceDataStore {
|
|||
public void storeSenderKey(
|
||||
final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record
|
||||
) {
|
||||
// TODO
|
||||
senderKeyStore.storeSenderKey(sender, distributionId, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) {
|
||||
// TODO
|
||||
return null;
|
||||
return senderKeyStore.loadSenderKey(sender, distributionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
|
||||
// TODO
|
||||
return null;
|
||||
return senderKeyStore.getSenderKeySharedWith(distributionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markSenderKeySharedWith(
|
||||
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
|
||||
) {
|
||||
// TODO
|
||||
senderKeyStore.markSenderKeySharedWith(distributionId, addresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
|
||||
// TODO
|
||||
senderKeyStore.clearSenderKeySharedWith(addresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
package org.asamk.signal.manager.storage.senderKeys;
|
||||
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups.state.SenderKeyStore {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(SenderKeyRecordStore.class);
|
||||
|
||||
private final Map<Key, SenderKeyRecord> cachedSenderKeys = new HashMap<>();
|
||||
|
||||
private final File senderKeysPath;
|
||||
|
||||
private final RecipientResolver resolver;
|
||||
|
||||
public SenderKeyRecordStore(
|
||||
final File senderKeysPath, final RecipientResolver resolver
|
||||
) {
|
||||
this.senderKeysPath = senderKeysPath;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SenderKeyRecord loadSenderKey(final SignalProtocolAddress address, final UUID distributionId) {
|
||||
final var key = getKey(address, distributionId);
|
||||
|
||||
synchronized (cachedSenderKeys) {
|
||||
return loadSenderKeyLocked(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSenderKey(
|
||||
final SignalProtocolAddress address, final UUID distributionId, final SenderKeyRecord record
|
||||
) {
|
||||
final var key = getKey(address, distributionId);
|
||||
|
||||
synchronized (cachedSenderKeys) {
|
||||
storeSenderKeyLocked(key, record);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteAll() {
|
||||
synchronized (cachedSenderKeys) {
|
||||
cachedSenderKeys.clear();
|
||||
final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches());
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final var file : files) {
|
||||
try {
|
||||
Files.delete(file.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete sender key file {}: {}", file, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteAllFor(final RecipientId recipientId) {
|
||||
synchronized (cachedSenderKeys) {
|
||||
cachedSenderKeys.clear();
|
||||
final var keys = getKeysLocked(recipientId);
|
||||
for (var key : keys) {
|
||||
deleteSenderKeyLocked(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||
synchronized (cachedSenderKeys) {
|
||||
final var keys = getKeysLocked(toBeMergedRecipientId);
|
||||
final var otherHasSenderKeys = keys.size() > 0;
|
||||
if (!otherHasSenderKeys) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Only to be merged recipient had sender keys, re-assigning to the new recipient.");
|
||||
for (var key : keys) {
|
||||
final var toBeMergedSenderKey = loadSenderKeyLocked(key);
|
||||
deleteSenderKeyLocked(key);
|
||||
if (toBeMergedSenderKey == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final var newKey = new Key(recipientId, key.getDeviceId(), key.distributionId);
|
||||
final var senderKeyRecord = loadSenderKeyLocked(newKey);
|
||||
if (senderKeyRecord != null) {
|
||||
continue;
|
||||
}
|
||||
storeSenderKeyLocked(newKey, senderKeyRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param identifier can be either a serialized uuid or a e164 phone number
|
||||
*/
|
||||
private RecipientId resolveRecipient(String identifier) {
|
||||
return resolver.resolveRecipient(identifier);
|
||||
}
|
||||
|
||||
private Key getKey(final SignalProtocolAddress address, final UUID distributionId) {
|
||||
final var recipientId = resolveRecipient(address.getName());
|
||||
return new Key(recipientId, address.getDeviceId(), distributionId);
|
||||
}
|
||||
|
||||
private List<Key> getKeysLocked(RecipientId recipientId) {
|
||||
final var files = senderKeysPath.listFiles((_file, s) -> s.startsWith(recipientId.getId() + "_"));
|
||||
if (files == null) {
|
||||
return List.of();
|
||||
}
|
||||
return parseFileNames(files);
|
||||
}
|
||||
|
||||
final Pattern senderKeyFileNamePattern = Pattern.compile("([0-9]+)_([0-9]+)_([0-9a-z\\-]+)");
|
||||
|
||||
private List<Key> parseFileNames(final File[] files) {
|
||||
return Arrays.stream(files)
|
||||
.map(f -> senderKeyFileNamePattern.matcher(f.getName()))
|
||||
.filter(Matcher::matches)
|
||||
.map(matcher -> new Key(RecipientId.of(Long.parseLong(matcher.group(1))),
|
||||
Integer.parseInt(matcher.group(2)),
|
||||
UUID.fromString(matcher.group(3))))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private File getSenderKeyFile(Key key) {
|
||||
try {
|
||||
IOUtils.createPrivateDirectories(senderKeysPath);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Failed to create sender keys path", e);
|
||||
}
|
||||
return new File(senderKeysPath,
|
||||
key.getRecipientId().getId() + "_" + key.getDeviceId() + "_" + key.distributionId.toString());
|
||||
}
|
||||
|
||||
private SenderKeyRecord loadSenderKeyLocked(final Key key) {
|
||||
{
|
||||
final var senderKeyRecord = cachedSenderKeys.get(key);
|
||||
if (senderKeyRecord != null) {
|
||||
return senderKeyRecord;
|
||||
}
|
||||
}
|
||||
|
||||
final var file = getSenderKeyFile(key);
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
try (var inputStream = new FileInputStream(file)) {
|
||||
final var senderKeyRecord = new SenderKeyRecord(inputStream.readAllBytes());
|
||||
cachedSenderKeys.put(key, senderKeyRecord);
|
||||
return senderKeyRecord;
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to load sender key, resetting sender key: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void storeSenderKeyLocked(final Key key, final SenderKeyRecord senderKeyRecord) {
|
||||
cachedSenderKeys.put(key, senderKeyRecord);
|
||||
|
||||
final var file = getSenderKeyFile(key);
|
||||
try {
|
||||
try (var outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(senderKeyRecord.serialize());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to store sender key, trying to delete file and retry: {}", e.getMessage());
|
||||
try {
|
||||
Files.delete(file.toPath());
|
||||
try (var outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(senderKeyRecord.serialize());
|
||||
}
|
||||
} catch (IOException e2) {
|
||||
logger.error("Failed to store sender key file {}: {}", file, e2.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteSenderKeyLocked(final Key key) {
|
||||
cachedSenderKeys.remove(key);
|
||||
|
||||
final var file = getSenderKeyFile(key);
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.delete(file.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete sender key file {}: {}", file, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Key {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final int deviceId;
|
||||
private final UUID distributionId;
|
||||
|
||||
public Key(
|
||||
final RecipientId recipientId, final int deviceId, final UUID distributionId
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.deviceId = deviceId;
|
||||
this.distributionId = distributionId;
|
||||
}
|
||||
|
||||
public RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public UUID getDistributionId() {
|
||||
return distributionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final Key key = (Key) o;
|
||||
|
||||
if (deviceId != key.deviceId) return false;
|
||||
if (!recipientId.equals(key.recipientId)) return false;
|
||||
return distributionId.equals(key.distributionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = recipientId.hashCode();
|
||||
result = 31 * result + deviceId;
|
||||
result = 31 * result + distributionId.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
package org.asamk.signal.manager.storage.senderKeys;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||
import org.asamk.signal.manager.storage.Utils;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SenderKeySharedStore {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class);
|
||||
|
||||
private final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final File file;
|
||||
|
||||
private final RecipientResolver resolver;
|
||||
private final RecipientAddressResolver addressResolver;
|
||||
|
||||
public static SenderKeySharedStore load(
|
||||
final File file, final RecipientAddressResolver addressResolver, final RecipientResolver resolver
|
||||
) throws IOException {
|
||||
final var objectMapper = Utils.createStorageObjectMapper();
|
||||
try (var inputStream = new FileInputStream(file)) {
|
||||
final var storage = objectMapper.readValue(inputStream, Storage.class);
|
||||
final var sharedSenderKeys = new HashMap<DistributionId, Set<SenderKeySharedEntry>>();
|
||||
for (final var senderKey : storage.sharedSenderKeys) {
|
||||
final var entry = new SenderKeySharedEntry(RecipientId.of(senderKey.recipientId), senderKey.deviceId);
|
||||
final var uuid = UuidUtil.parseOrNull(senderKey.distributionId);
|
||||
if (uuid == null) {
|
||||
logger.warn("Read invalid distribution id from storage {}, ignoring", senderKey.distributionId);
|
||||
continue;
|
||||
}
|
||||
final var distributionId = DistributionId.from(uuid);
|
||||
var entries = sharedSenderKeys.get(distributionId);
|
||||
if (entries == null) {
|
||||
entries = new HashSet<>();
|
||||
}
|
||||
entries.add(entry);
|
||||
sharedSenderKeys.put(distributionId, entries);
|
||||
}
|
||||
|
||||
return new SenderKeySharedStore(sharedSenderKeys, objectMapper, file, addressResolver, resolver);
|
||||
} catch (FileNotFoundException e) {
|
||||
logger.debug("Creating new shared sender key store.");
|
||||
return new SenderKeySharedStore(new HashMap<>(), objectMapper, file, addressResolver, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
private SenderKeySharedStore(
|
||||
final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys,
|
||||
final ObjectMapper objectMapper,
|
||||
final File file,
|
||||
final RecipientAddressResolver addressResolver,
|
||||
final RecipientResolver resolver
|
||||
) {
|
||||
this.sharedSenderKeys = sharedSenderKeys;
|
||||
this.objectMapper = objectMapper;
|
||||
this.file = file;
|
||||
this.addressResolver = addressResolver;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
|
||||
synchronized (sharedSenderKeys) {
|
||||
return sharedSenderKeys.get(distributionId)
|
||||
.stream()
|
||||
.map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.getRecipientId())
|
||||
.getIdentifier(), k.getDeviceId()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
public void markSenderKeySharedWith(
|
||||
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
|
||||
) {
|
||||
final var newEntries = addresses.stream()
|
||||
.map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
synchronized (sharedSenderKeys) {
|
||||
final var previousEntries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
|
||||
|
||||
sharedSenderKeys.put(distributionId, new HashSet<>() {
|
||||
{
|
||||
addAll(previousEntries);
|
||||
addAll(newEntries);
|
||||
}
|
||||
});
|
||||
saveLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
|
||||
final var entriesToDelete = addresses.stream()
|
||||
.map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
synchronized (sharedSenderKeys) {
|
||||
for (final var distributionId : sharedSenderKeys.keySet()) {
|
||||
final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
|
||||
|
||||
sharedSenderKeys.put(distributionId, new HashSet<>(entries) {
|
||||
{
|
||||
removeAll(entriesToDelete);
|
||||
}
|
||||
});
|
||||
}
|
||||
saveLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteAll() {
|
||||
synchronized (sharedSenderKeys) {
|
||||
sharedSenderKeys.clear();
|
||||
saveLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteAllFor(final RecipientId recipientId) {
|
||||
synchronized (sharedSenderKeys) {
|
||||
for (final var distributionId : sharedSenderKeys.keySet()) {
|
||||
final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
|
||||
|
||||
sharedSenderKeys.put(distributionId, new HashSet<>(entries) {
|
||||
{
|
||||
entries.removeIf(e -> e.getRecipientId().equals(recipientId));
|
||||
}
|
||||
});
|
||||
}
|
||||
saveLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||
synchronized (sharedSenderKeys) {
|
||||
for (final var distributionId : sharedSenderKeys.keySet()) {
|
||||
final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of());
|
||||
|
||||
sharedSenderKeys.put(distributionId,
|
||||
entries.stream()
|
||||
.map(e -> e.recipientId.equals(toBeMergedRecipientId) ? new SenderKeySharedEntry(
|
||||
recipientId,
|
||||
e.getDeviceId()) : e)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
saveLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param identifier can be either a serialized uuid or a e164 phone number
|
||||
*/
|
||||
private RecipientId resolveRecipient(String identifier) {
|
||||
return resolver.resolveRecipient(identifier);
|
||||
}
|
||||
|
||||
private void saveLocked() {
|
||||
var storage = new Storage(sharedSenderKeys.entrySet().stream().flatMap(pair -> {
|
||||
final var sharedWith = pair.getValue();
|
||||
return sharedWith.stream()
|
||||
.map(entry -> new Storage.SharedSenderKey(entry.getRecipientId().getId(),
|
||||
entry.getDeviceId(),
|
||||
pair.getKey().asUuid().toString()));
|
||||
}).collect(Collectors.toList()));
|
||||
|
||||
// Write to memory first to prevent corrupting the file in case of serialization errors
|
||||
try (var inMemoryOutput = new ByteArrayOutputStream()) {
|
||||
objectMapper.writeValue(inMemoryOutput, storage);
|
||||
|
||||
var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
|
||||
try (var outputStream = new FileOutputStream(file)) {
|
||||
input.transferTo(outputStream);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error saving shared sender key store file: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static class Storage {
|
||||
|
||||
public List<SharedSenderKey> sharedSenderKeys;
|
||||
|
||||
// For deserialization
|
||||
private Storage() {
|
||||
}
|
||||
|
||||
public Storage(final List<SharedSenderKey> sharedSenderKeys) {
|
||||
this.sharedSenderKeys = sharedSenderKeys;
|
||||
}
|
||||
|
||||
private static class SharedSenderKey {
|
||||
|
||||
public long recipientId;
|
||||
public int deviceId;
|
||||
public String distributionId;
|
||||
|
||||
// For deserialization
|
||||
private SharedSenderKey() {
|
||||
}
|
||||
|
||||
public SharedSenderKey(final long recipientId, final int deviceId, final String distributionId) {
|
||||
this.recipientId = recipientId;
|
||||
this.deviceId = deviceId;
|
||||
this.distributionId = distributionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SenderKeySharedEntry {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final int deviceId;
|
||||
|
||||
public SenderKeySharedEntry(
|
||||
final RecipientId recipientId, final int deviceId
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final SenderKeySharedEntry that = (SenderKeySharedEntry) o;
|
||||
|
||||
if (deviceId != that.deviceId) return false;
|
||||
return recipientId.equals(that.recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = recipientId.hashCode();
|
||||
result = 31 * result + deviceId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package org.asamk.signal.manager.storage.senderKeys;
|
||||
|
||||
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.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
|
||||
import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SenderKeyStore implements SignalServiceSenderKeyStore {
|
||||
|
||||
private final SenderKeyRecordStore senderKeyRecordStore;
|
||||
private final SenderKeySharedStore senderKeySharedStore;
|
||||
|
||||
public SenderKeyStore(
|
||||
final File file,
|
||||
final File senderKeysPath,
|
||||
final RecipientAddressResolver addressResolver,
|
||||
final RecipientResolver resolver
|
||||
) throws IOException {
|
||||
this.senderKeyRecordStore = new SenderKeyRecordStore(senderKeysPath, resolver);
|
||||
this.senderKeySharedStore = SenderKeySharedStore.load(file, addressResolver, resolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSenderKey(
|
||||
final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record
|
||||
) {
|
||||
senderKeyRecordStore.storeSenderKey(sender, distributionId, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) {
|
||||
return senderKeyRecordStore.loadSenderKey(sender, distributionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
|
||||
return senderKeySharedStore.getSenderKeySharedWith(distributionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markSenderKeySharedWith(
|
||||
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
|
||||
) {
|
||||
senderKeySharedStore.markSenderKeySharedWith(distributionId, addresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
|
||||
senderKeySharedStore.clearSenderKeySharedWith(addresses);
|
||||
}
|
||||
|
||||
public void deleteAll() {
|
||||
senderKeySharedStore.deleteAll();
|
||||
senderKeyRecordStore.deleteAll();
|
||||
}
|
||||
|
||||
public void rotateSenderKeys(RecipientId recipientId) {
|
||||
senderKeySharedStore.deleteAllFor(recipientId);
|
||||
senderKeyRecordStore.deleteAllFor(recipientId);
|
||||
}
|
||||
|
||||
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||
senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
}
|
||||
}
|
|
@ -82,6 +82,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
|||
DateUtils.formatTimestamp(content.getServerReceivedTimestamp()),
|
||||
DateUtils.formatTimestamp(content.getServerDeliveredTimestamp()));
|
||||
|
||||
if (content.getSenderKeyDistributionMessage().isPresent()) {
|
||||
final var message = content.getSenderKeyDistributionMessage().get();
|
||||
writer.println("Received a sender key distribution message for distributionId {}",
|
||||
message.getDistributionId());
|
||||
}
|
||||
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
var message = content.getDataMessage().get();
|
||||
printDataMessage(writer, message);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue