package org.asamk.signal.manager.storage;
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.KeyUtils;
-import org.asamk.signal.manager.util.Utils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
- private final ObjectMapper jsonProcessor = new ObjectMapper();
+ private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper();
+
private final FileChannel fileChannel;
private final FileLock lock;
+
private String username;
private UUID uuid;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
private JsonGroupStore groupStore;
private RecipientStore recipientStore;
private StickerStore stickerStore;
+ private StickerStore.Storage stickerStoreStorage;
private MessageCache messageCache;
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
this.fileChannel = fileChannel;
this.lock = lock;
- jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
- jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
- jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
- jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
- jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
public static SignalAccount load(File dataPath, String username) throws IOException {
account.username = username;
account.profileKey = profileKey;
- account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
- account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
- account::mergeRecipients);
- account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
- account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
- account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
- account.recipientStore::resolveRecipient);
- account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
- account.recipientStore::resolveRecipient,
- identityKey,
- registrationId);
- account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
- account.signedPreKeyStore,
- account.sessionStore,
- account.identityKeyStore);
- account.stickerStore = new StickerStore();
- account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
+ account.initStores(dataPath, identityKey, registrationId);
+ account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
+ account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = false;
return account;
}
+ private void initStores(
+ final File dataPath, final IdentityKeyPair identityKey, final int registrationId
+ ) throws IOException {
+ recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
+
+ preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
+ signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
+ sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
+ identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
+ recipientStore::resolveRecipient,
+ identityKey,
+ registrationId);
+ signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
+
+ messageCache = new MessageCache(getMessageCachePath(dataPath, username));
+ }
+
public static SignalAccount createLinkedAccount(
File dataPath,
String username,
account.password = password;
account.profileKey = profileKey;
account.deviceId = deviceId;
- account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
- account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
- account::mergeRecipients);
- account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
- account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
- account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
- account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
- account.recipientStore::resolveRecipient);
- account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
- account.recipientStore::resolveRecipient,
- identityKey,
- registrationId);
- account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
- account.signedPreKeyStore,
- account.sessionStore,
- account.identityKeyStore);
- account.stickerStore = new StickerStore();
- account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
+ account.initStores(dataPath, identityKey, registrationId);
+ account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
+ account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = true;
account.isMultiDevice = true;
+ account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.migrateLegacyConfigs();
return account;
registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
}
- recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
-
- preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
- signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
- sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
- identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
- recipientStore::resolveRecipient,
- identityKeyPair,
- registrationId);
+ initStores(dataPath, identityKeyPair, registrationId);
loadLegacyStores(rootNode, legacySignalProtocolStore);
- signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
-
var groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
}
- var stickerStoreNode = rootNode.get("stickerStore");
- if (stickerStoreNode != null) {
- stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
- }
- if (stickerStore == null) {
- stickerStore = new StickerStore();
+ if (rootNode.hasNonNull("stickerStore")) {
+ stickerStoreStorage = jsonProcessor.convertValue(rootNode.get("stickerStore"), StickerStore.Storage.class);
+ stickerStore = StickerStore.fromStorage(stickerStoreStorage, this::saveStickerStore);
+ } else {
+ stickerStore = new StickerStore(this::saveStickerStore);
}
- messageCache = new MessageCache(getMessageCachePath(dataPath, username));
-
loadLegacyThreadStore(rootNode);
}
}
}
+ private void saveStickerStore(StickerStore.Storage storage) {
+ this.stickerStoreStorage = storage;
+ save();
+ }
+
public void save() {
- if (fileChannel == null) {
- return;
- }
- var rootNode = jsonProcessor.createObjectNode();
- rootNode.put("username", username)
- .put("uuid", uuid == null ? null : uuid.toString())
- .put("deviceId", deviceId)
- .put("isMultiDevice", isMultiDevice)
- .put("password", password)
- .put("registrationId", identityKeyStore.getLocalRegistrationId())
- .put("identityPrivateKey",
- Base64.getEncoder()
- .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
- .put("identityKey",
- Base64.getEncoder()
- .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
- .put("registrationLockPin", registrationLockPin)
- .put("pinMasterKey",
- pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
- .put("storageKey",
- storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
- .put("preKeyIdOffset", preKeyIdOffset)
- .put("nextSignedPreKeyId", nextSignedPreKeyId)
- .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
- .put("registered", registered)
- .putPOJO("groupStore", groupStore)
- .putPOJO("stickerStore", stickerStore);
- try {
- try (var output = new ByteArrayOutputStream()) {
- // Write to memory first to prevent corrupting the file in case of serialization errors
- jsonProcessor.writeValue(output, rootNode);
- var input = new ByteArrayInputStream(output.toByteArray());
- synchronized (fileChannel) {
+ synchronized (fileChannel) {
+ var rootNode = jsonProcessor.createObjectNode();
+ rootNode.put("username", username)
+ .put("uuid", uuid == null ? null : uuid.toString())
+ .put("deviceId", deviceId)
+ .put("isMultiDevice", isMultiDevice)
+ .put("password", password)
+ .put("registrationId", identityKeyStore.getLocalRegistrationId())
+ .put("identityPrivateKey",
+ Base64.getEncoder()
+ .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
+ .put("identityKey",
+ Base64.getEncoder()
+ .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
+ .put("registrationLockPin", registrationLockPin)
+ .put("pinMasterKey",
+ pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
+ .put("storageKey",
+ storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
+ .put("preKeyIdOffset", preKeyIdOffset)
+ .put("nextSignedPreKeyId", nextSignedPreKeyId)
+ .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
+ .put("registered", registered)
+ .putPOJO("groupStore", groupStore)
+ .putPOJO("stickerStore", stickerStoreStorage);
+ try {
+ try (var output = new ByteArrayOutputStream()) {
+ // Write to memory first to prevent corrupting the file in case of serialization errors
+ jsonProcessor.writeValue(output, rootNode);
+ var input = new ByteArrayInputStream(output.toByteArray());
fileChannel.position(0);
input.transferTo(Channels.newOutputStream(fileChannel));
fileChannel.truncate(fileChannel.position());
fileChannel.force(false);
}
+ } catch (Exception e) {
+ logger.error("Error saving file: {}", e.getMessage());
}
- } catch (Exception e) {
- logger.error("Error saving file: {}", e.getMessage());
}
}
package org.asamk.signal.manager.storage.stickers;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-
-import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
public class StickerStore {
- @JsonSerialize(using = StickersSerializer.class)
- @JsonDeserialize(using = StickersDeserializer.class)
- private final Map<byte[], Sticker> stickers = new HashMap<>();
+ private final Map<StickerPackId, Sticker> stickers;
- public Sticker getSticker(byte[] packId) {
- return stickers.get(packId);
+ private final Saver saver;
+
+ public StickerStore(final Saver saver) {
+ this.saver = saver;
+ stickers = new HashMap<>();
}
- public void updateSticker(Sticker sticker) {
- stickers.put(sticker.getPackId(), sticker);
+ public StickerStore(final Map<StickerPackId, Sticker> stickers, final Saver saver) {
+ this.stickers = stickers;
+ this.saver = saver;
}
- private static class StickersSerializer extends JsonSerializer<Map<byte[], Sticker>> {
-
- @Override
- public void serialize(
- final Map<byte[], Sticker> value, final JsonGenerator jgen, final SerializerProvider provider
- ) throws IOException {
- final var stickers = value.values();
- jgen.writeStartArray(stickers.size());
- for (var sticker : stickers) {
- jgen.writeStartObject();
- jgen.writeStringField("packId", Base64.getEncoder().encodeToString(sticker.getPackId()));
- jgen.writeStringField("packKey", Base64.getEncoder().encodeToString(sticker.getPackKey()));
- jgen.writeBooleanField("installed", sticker.isInstalled());
- jgen.writeEndObject();
+ public static StickerStore fromStorage(Storage storage, Saver saver) {
+ final var packIds = new HashSet<StickerPackId>();
+ final var stickers = storage.stickers.stream().map(s -> {
+ var packId = StickerPackId.deserialize(Base64.getDecoder().decode(s.packId));
+ if (packIds.contains(packId)) {
+ // Remove legacy duplicate packIds ...
+ return null;
}
- jgen.writeEndArray();
+ packIds.add(packId);
+ var packKey = Base64.getDecoder().decode(s.packKey);
+ var installed = s.installed;
+ return new Sticker(packId, packKey, installed);
+ }).filter(Objects::nonNull).collect(Collectors.toMap(Sticker::getPackId, s -> s));
+
+ return new StickerStore(stickers, saver);
+ }
+
+ public Sticker getSticker(StickerPackId packId) {
+ synchronized (stickers) {
+ return stickers.get(packId);
+ }
+ }
+
+ public void updateSticker(Sticker sticker) {
+ Storage storage;
+ synchronized (stickers) {
+ stickers.put(sticker.getPackId(), sticker);
+ storage = toStorageLocked();
}
+ saver.save(storage);
}
- private static class StickersDeserializer extends JsonDeserializer<Map<byte[], Sticker>> {
-
- @Override
- public Map<byte[], Sticker> deserialize(
- JsonParser jsonParser, DeserializationContext deserializationContext
- ) throws IOException {
- var stickers = new HashMap<byte[], Sticker>();
- JsonNode node = jsonParser.getCodec().readTree(jsonParser);
- for (var n : node) {
- var packId = Base64.getDecoder().decode(n.get("packId").asText());
- var packKey = Base64.getDecoder().decode(n.get("packKey").asText());
- var installed = n.get("installed").asBoolean(false);
- stickers.put(packId, new Sticker(packId, packKey, installed));
+ private Storage toStorageLocked() {
+ return new Storage(stickers.values()
+ .stream()
+ .map(s -> new Storage.Sticker(Base64.getEncoder().encodeToString(s.getPackId().serialize()),
+ Base64.getEncoder().encodeToString(s.getPackKey()),
+ s.isInstalled()))
+ .collect(Collectors.toList()));
+ }
+
+ public static class Storage {
+
+ public List<Storage.Sticker> stickers;
+
+ // For deserialization
+ private Storage() {
+ }
+
+ public Storage(final List<Sticker> stickers) {
+ this.stickers = stickers;
+ }
+
+ private static class Sticker {
+
+ public String packId;
+ public String packKey;
+ public boolean installed;
+
+ // For deserialization
+ private Sticker() {
}
- return stickers;
+ public Sticker(final String packId, final String packKey, final boolean installed) {
+ this.packId = packId;
+ this.packKey = packKey;
+ this.installed = installed;
+ }
}
}
+
+ public interface Saver {
+
+ void save(Storage storage);
+ }
}