From: AsamK Date: Sat, 17 Apr 2021 14:06:35 +0000 (+0200) Subject: Refactor signed pre key store X-Git-Tag: v0.8.2~45 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/afb22deadaa4c8058db65af7e65b242568319bf6?ds=sidebyside Refactor signed pre key store --- diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 494a7e89..298dd1ed 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -16,6 +16,7 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.JsonGroupStore; import org.asamk.signal.manager.storage.messageCache.MessageCache; import org.asamk.signal.manager.storage.prekeys.PreKeyStore; +import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore; import org.asamk.signal.manager.storage.profiles.ProfileStore; import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver; @@ -82,6 +83,7 @@ public class SignalAccount implements Closeable { private JsonSignalProtocolStore signalProtocolStore; private PreKeyStore preKeyStore; + private SignedPreKeyStore signedPreKeyStore; private SessionStore sessionStore; private JsonGroupStore groupStore; private JsonContactsStore contactStore; @@ -136,11 +138,13 @@ public class SignalAccount implements Closeable { 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.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId, account.preKeyStore, + account.signedPreKeyStore, account.sessionStore); account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); @@ -183,11 +187,13 @@ public class SignalAccount implements Closeable { 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.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId, account.preKeyStore, + account.signedPreKeyStore, account.sessionStore); account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); @@ -251,6 +257,10 @@ public class SignalAccount implements Closeable { return new File(getUserPath(dataPath, username), "pre-keys"); } + private static File getSignedPreKeysPath(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "signed-pre-keys"); + } + private static File getSessionsPath(File dataPath, String username) { return new File(getUserPath(dataPath, username), "sessions"); } @@ -330,6 +340,7 @@ public class SignalAccount implements Closeable { signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); + preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username)); if (signalProtocolStore.getLegacyPreKeyStore() != null) { logger.debug("Migrating legacy pre key store."); @@ -342,6 +353,20 @@ public class SignalAccount implements Closeable { } } signalProtocolStore.setPreKeyStore(preKeyStore); + + signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username)); + if (signalProtocolStore.getLegacySignedPreKeyStore() != null) { + logger.debug("Migrating legacy signed pre key store."); + for (var entry : signalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) { + try { + signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue())); + } catch (IOException e) { + logger.warn("Failed to migrate signed pre key, ignoring", e); + } + } + } + signalProtocolStore.setSignedPreKeyStore(signedPreKeyStore); + sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient); if (signalProtocolStore.getLegacySessionStore() != null) { logger.debug("Migrating legacy session store."); @@ -355,6 +380,7 @@ public class SignalAccount implements Closeable { } } signalProtocolStore.setSessionStore(sessionStore); + registered = Utils.getNotNullNode(rootNode, "registered").asBoolean(); var groupStoreNode = rootNode.get("groupStore"); if (groupStoreNode != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java new file mode 100644 index 00000000..83176557 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/prekeys/SignedPreKeyStore.java @@ -0,0 +1,111 @@ +package org.asamk.signal.manager.storage.prekeys; + +import org.asamk.signal.manager.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; + +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.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class SignedPreKeyStore implements org.whispersystems.libsignal.state.SignedPreKeyStore { + + private final static Logger logger = LoggerFactory.getLogger(SignedPreKeyStore.class); + + private final File signedPreKeysPath; + + public SignedPreKeyStore(final File signedPreKeysPath) { + this.signedPreKeysPath = signedPreKeysPath; + } + + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + final var file = getSignedPreKeyFile(signedPreKeyId); + + if (!file.exists()) { + throw new InvalidKeyIdException("No such signed pre key record!"); + } + return loadSignedPreKeyRecord(file); + } + + final Pattern signedPreKeyFileNamePattern = Pattern.compile("([0-9]+)"); + + @Override + public List loadSignedPreKeys() { + final var files = signedPreKeysPath.listFiles(); + if (files == null) { + return List.of(); + } + return Arrays.stream(files) + .filter(f -> signedPreKeyFileNamePattern.matcher(f.getName()).matches()) + .map(this::loadSignedPreKeyRecord) + .collect(Collectors.toList()); + } + + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + final var file = getSignedPreKeyFile(signedPreKeyId); + try { + try (var outputStream = new FileOutputStream(file)) { + outputStream.write(record.serialize()); + } + } catch (IOException e) { + logger.warn("Failed to store signed pre key, trying to delete file and retry: {}", e.getMessage()); + try { + Files.delete(file.toPath()); + try (var outputStream = new FileOutputStream(file)) { + outputStream.write(record.serialize()); + } + } catch (IOException e2) { + logger.error("Failed to store signed pre key file {}: {}", file, e2.getMessage()); + } + } + } + + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + final var file = getSignedPreKeyFile(signedPreKeyId); + + return file.exists(); + } + + @Override + public void removeSignedPreKey(int signedPreKeyId) { + final var file = getSignedPreKeyFile(signedPreKeyId); + + if (!file.exists()) { + return; + } + try { + Files.delete(file.toPath()); + } catch (IOException e) { + logger.error("Failed to delete signed pre key file {}: {}", file, e.getMessage()); + } + } + + private File getSignedPreKeyFile(int signedPreKeyId) { + try { + IOUtils.createPrivateDirectories(signedPreKeysPath); + } catch (IOException e) { + throw new AssertionError("Failed to create signed pre keys path", e); + } + return new File(signedPreKeysPath, String.valueOf(signedPreKeyId)); + } + + private SignedPreKeyRecord loadSignedPreKeyRecord(final File file) { + try (var inputStream = new FileInputStream(file)) { + return new SignedPreKeyRecord(inputStream.readAllBytes()); + } catch (IOException e) { + logger.error("Failed to load signed pre key: {}", e.getMessage()); + throw new AssertionError(e); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java index 29e5c031..91318084 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java @@ -14,13 +14,14 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyStore; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.signalservice.api.SignalServiceProtocolStore; import org.whispersystems.signalservice.api.SignalServiceSessionStore; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.List; -@JsonIgnoreProperties(value = {"sessionStore", "preKeys"}, allowSetters = true) +@JsonIgnoreProperties(value = {"sessionStore", "preKeys", "signedPreKeyStore"}, allowSetters = true) public class JsonSignalProtocolStore implements SignalServiceProtocolStore { @JsonProperty("preKeys") @@ -32,9 +33,8 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore { private LegacyJsonSessionStore legacySessionStore; @JsonProperty("signedPreKeyStore") - @JsonDeserialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class) - @JsonSerialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreSerializer.class) - private JsonSignedPreKeyStore signedPreKeyStore; + @JsonDeserialize(using = LegacyJsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class) + private LegacyJsonSignedPreKeyStore legacySignedPreKeyStore; @JsonProperty("identityKeyStore") @JsonDeserialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class) @@ -42,6 +42,7 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore { private JsonIdentityKeyStore identityKeyStore; private PreKeyStore preKeyStore; + private SignedPreKeyStore signedPreKeyStore; private SignalServiceSessionStore sessionStore; public JsonSignalProtocolStore() { @@ -51,11 +52,12 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore { IdentityKeyPair identityKeyPair, int registrationId, PreKeyStore preKeyStore, + SignedPreKeyStore signedPreKeyStore, SignalServiceSessionStore sessionStore ) { this.preKeyStore = preKeyStore; + this.signedPreKeyStore = signedPreKeyStore; this.sessionStore = sessionStore; - signedPreKeyStore = new JsonSignedPreKeyStore(); this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId); } @@ -67,6 +69,10 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore { this.preKeyStore = preKeyStore; } + public void setSignedPreKeyStore(final SignedPreKeyStore signedPreKeyStore) { + this.signedPreKeyStore = signedPreKeyStore; + } + public void setSessionStore(final SignalServiceSessionStore sessionStore) { this.sessionStore = sessionStore; } @@ -75,6 +81,10 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore { return legacyPreKeyStore; } + public LegacyJsonSignedPreKeyStore getLegacySignedPreKeyStore() { + return legacySignedPreKeyStore; + } + public LegacyJsonSessionStore getLegacySessionStore() { return legacySessionStore; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java deleted file mode 100644 index 655e372a..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.asamk.signal.manager.storage.protocol; - -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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyStore; - -import java.io.IOException; -import java.util.Base64; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -class JsonSignedPreKeyStore implements SignedPreKeyStore { - - private final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class); - - private final Map store = new HashMap<>(); - - public JsonSignedPreKeyStore() { - - } - - private void addSignedPreKeys(Map preKeys) { - store.putAll(preKeys); - } - - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - try { - if (!store.containsKey(signedPreKeyId)) { - throw new InvalidKeyIdException("No such signedprekeyrecord! " + signedPreKeyId); - } - - return new SignedPreKeyRecord(store.get(signedPreKeyId)); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - @Override - public List loadSignedPreKeys() { - try { - var results = new LinkedList(); - - for (var serialized : store.values()) { - results.add(new SignedPreKeyRecord(serialized)); - } - - return results; - } catch (IOException e) { - throw new AssertionError(e); - } - } - - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - store.put(signedPreKeyId, record.serialize()); - } - - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return store.containsKey(signedPreKeyId); - } - - @Override - public void removeSignedPreKey(int signedPreKeyId) { - store.remove(signedPreKeyId); - } - - public static class JsonSignedPreKeyStoreDeserializer extends JsonDeserializer { - - @Override - public JsonSignedPreKeyStore deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext - ) throws IOException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - - var preKeyMap = new HashMap(); - if (node.isArray()) { - for (var preKey : node) { - final var preKeyId = preKey.get("id").asInt(); - final var preKeyRecord = Base64.getDecoder().decode(preKey.get("record").asText()); - preKeyMap.put(preKeyId, preKeyRecord); - } - } - - var keyStore = new JsonSignedPreKeyStore(); - keyStore.addSignedPreKeys(preKeyMap); - - return keyStore; - } - } - - public static class JsonSignedPreKeyStoreSerializer extends JsonSerializer { - - @Override - public void serialize( - JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider - ) throws IOException { - json.writeStartArray(); - for (var signedPreKey : jsonPreKeyStore.store.entrySet()) { - json.writeStartObject(); - json.writeNumberField("id", signedPreKey.getKey()); - json.writeStringField("record", Base64.getEncoder().encodeToString(signedPreKey.getValue())); - json.writeEndObject(); - } - json.writeEndArray(); - } - } -} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java index 24101e10..7412f95a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonPreKeyStore.java @@ -14,7 +14,7 @@ public class LegacyJsonPreKeyStore { private final Map preKeys; - public LegacyJsonPreKeyStore(final Map preKeys) { + private LegacyJsonPreKeyStore(final Map preKeys) { this.preKeys = preKeys; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java index c43bdb08..9a14ab60 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java @@ -16,9 +16,10 @@ import java.util.List; public class LegacyJsonSessionStore { - private final List sessions = new ArrayList<>(); + private final List sessions; - public LegacyJsonSessionStore() { + private LegacyJsonSessionStore(final List sessions) { + this.sessions = sessions; } public List getSessions() { @@ -33,7 +34,7 @@ public class LegacyJsonSessionStore { ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - var sessionStore = new LegacyJsonSessionStore(); + var sessions = new ArrayList(); if (node.isArray()) { for (var session : node) { @@ -50,11 +51,11 @@ public class LegacyJsonSessionStore { final var deviceId = session.get("deviceId").asInt(); final var record = Base64.getDecoder().decode(session.get("record").asText()); var sessionInfo = new SessionInfo(serviceAddress, deviceId, record); - sessionStore.sessions.add(sessionInfo); + sessions.add(sessionInfo); } } - return sessionStore; + return new LegacyJsonSessionStore(sessions); } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignedPreKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignedPreKeyStore.java new file mode 100644 index 00000000..cdcba916 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSignedPreKeyStore.java @@ -0,0 +1,45 @@ +package org.asamk.signal.manager.storage.protocol; + +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 java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +public class LegacyJsonSignedPreKeyStore { + + private final Map signedPreKeys; + + private LegacyJsonSignedPreKeyStore(final Map signedPreKeys) { + this.signedPreKeys = signedPreKeys; + } + + public Map getSignedPreKeys() { + return signedPreKeys; + } + + public static class JsonSignedPreKeyStoreDeserializer extends JsonDeserializer { + + @Override + public LegacyJsonSignedPreKeyStore deserialize( + JsonParser jsonParser, DeserializationContext deserializationContext + ) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + var preKeyMap = new HashMap(); + if (node.isArray()) { + for (var preKey : node) { + final var preKeyId = preKey.get("id").asInt(); + final var preKeyRecord = Base64.getDecoder().decode(preKey.get("record").asText()); + preKeyMap.put(preKeyId, preKeyRecord); + } + } + + return new LegacyJsonSignedPreKeyStore(preKeyMap); + } + } +}