Refactor identity key store

This commit is contained in:
AsamK 2021-04-18 18:26:12 +02:00
parent afb22deada
commit 8a0c6cae15
19 changed files with 717 additions and 563 deletions

View file

@ -34,9 +34,10 @@ import org.asamk.signal.manager.storage.contacts.ContactInfo;
import org.asamk.signal.manager.storage.groups.GroupInfo;
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.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.Sticker;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils;
@ -234,8 +235,6 @@ public class Manager implements Closeable {
clientZkProfileOperations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
this.account.setResolver(this::resolveSignalServiceAddress);
this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
account.getProfileStore()::getProfileKey,
this::getRecipientProfile,
@ -1223,17 +1222,7 @@ public class Manager implements Closeable {
private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
var messageSender = createMessageSender();
try {
messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
} catch (UntrustedIdentityException e) {
if (e.getIdentityKey() != null) {
account.getSignalProtocolStore()
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
e.getIdentityKey(),
TrustLevel.UNTRUSTED);
}
throw e;
}
messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
}
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
@ -1303,22 +1292,8 @@ public class Manager implements Closeable {
unidentifiedAccessHelper.getAccessFor(recipients),
isRecipientUpdate,
message);
for (var r : result) {
if (r.getIdentityFailure() != null) {
account.getSignalProtocolStore()
.saveIdentity(r.getAddress(),
r.getIdentityFailure().getIdentityKey(),
TrustLevel.UNTRUSTED);
}
}
return new Pair<>(timestamp, result);
} catch (UntrustedIdentityException e) {
if (e.getIdentityKey() != null) {
account.getSignalProtocolStore()
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
e.getIdentityKey(),
TrustLevel.UNTRUSTED);
}
return new Pair<>(timestamp, List.of());
}
} else {
@ -1388,12 +1363,6 @@ public class Manager implements Closeable {
false,
System.currentTimeMillis() - startTime);
} catch (UntrustedIdentityException e) {
if (e.getIdentityKey() != null) {
account.getSignalProtocolStore()
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
e.getIdentityKey(),
TrustLevel.UNTRUSTED);
}
return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
}
}
@ -1406,12 +1375,6 @@ public class Manager implements Closeable {
try {
return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
} catch (UntrustedIdentityException e) {
if (e.getIdentityKey() != null) {
account.getSignalProtocolStore()
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
e.getIdentityKey(),
TrustLevel.UNTRUSTED);
}
return SendMessageResult.identityFailure(address, e.getIdentityKey());
}
}
@ -1424,15 +1387,7 @@ public class Manager implements Closeable {
return cipher.decrypt(envelope);
} catch (ProtocolUntrustedIdentityException e) {
if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
var identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
final var untrustedIdentity = identityException.getUntrustedIdentity();
if (untrustedIdentity != null) {
account.getSignalProtocolStore()
.saveIdentity(resolveSignalServiceAddress(identityException.getName()),
untrustedIdentity,
TrustLevel.UNTRUSTED);
}
throw identityException;
throw (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
}
throw new AssertionError(e);
}
@ -2004,8 +1959,8 @@ public class Manager implements Closeable {
}
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
account.getSignalProtocolStore()
.setIdentityTrustLevel(verifiedMessage.getDestination(),
account.getIdentityKeyStore()
.setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
@ -2040,8 +1995,8 @@ public class Manager implements Closeable {
}
if (syncMessage.getVerified().isPresent()) {
final var verifiedMessage = syncMessage.getVerified().get();
account.getSignalProtocolStore()
.setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()),
account.getIdentityKeyStore()
.setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
@ -2283,7 +2238,8 @@ public class Manager implements Closeable {
var out = new DeviceContactsOutputStream(fos);
for (var record : account.getContactStore().getContacts()) {
VerifiedMessage verifiedMessage = null;
var currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress());
var currentIdentity = account.getIdentityKeyStore()
.getIdentity(resolveRecipientTrusted(record.getAddress()));
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(record.getAddress(),
currentIdentity.getIdentityKey(),
@ -2395,11 +2351,12 @@ public class Manager implements Closeable {
}
public List<IdentityInfo> getIdentities() {
return account.getSignalProtocolStore().getIdentities();
return account.getIdentityKeyStore().getIdentities();
}
public List<IdentityInfo> getIdentities(String number) throws InvalidNumberException {
return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number));
final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number));
return identity == null ? List.of() : List.of(identity);
}
/**
@ -2409,8 +2366,10 @@ public class Manager implements Closeable {
* @param fingerprint Fingerprint
*/
public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
var address = canonicalizeAndResolveSignalServiceAddress(name);
return trustIdentity(address, (identityKey) -> Arrays.equals(identityKey.serialize(), fingerprint));
var recipientId = canonicalizeAndResolveRecipient(name);
return trustIdentity(recipientId,
identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
TrustLevel.TRUSTED_VERIFIED);
}
/**
@ -2420,47 +2379,11 @@ public class Manager implements Closeable {
* @param safetyNumber Safety number
*/
public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
var address = canonicalizeAndResolveSignalServiceAddress(name);
return trustIdentity(address, (identityKey) -> safetyNumber.equals(computeSafetyNumber(address, identityKey)));
}
private boolean trustIdentity(SignalServiceAddress address, Function<IdentityKey, Boolean> verifier) {
var ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
return false;
}
IdentityInfo foundIdentity = null;
for (var id : ids) {
if (verifier.apply(id.getIdentityKey())) {
foundIdentity = id;
break;
}
}
if (foundIdentity == null) {
return false;
}
account.getSignalProtocolStore()
.setIdentityTrustLevel(address, foundIdentity.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
try {
sendVerifiedMessage(address, foundIdentity.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
logger.warn("Failed to send verification sync message: {}", e.getMessage());
}
// Successfully trusted the new identity, now remove all other identities for that number
for (var id : ids) {
if (id == foundIdentity) {
continue;
}
account.getSignalProtocolStore().removeIdentity(address, id.getIdentityKey());
}
account.save();
return true;
var recipientId = canonicalizeAndResolveRecipient(name);
var address = account.getRecipientStore().resolveServiceAddress(recipientId);
return trustIdentity(recipientId,
identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
TrustLevel.TRUSTED_VERIFIED);
}
/**
@ -2468,24 +2391,31 @@ public class Manager implements Closeable {
*
* @param name username of the identity
*/
public boolean trustIdentityAllKeys(String name) {
var address = resolveSignalServiceAddress(name);
var ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
public boolean trustIdentityAllKeys(String name) throws InvalidNumberException {
var recipientId = canonicalizeAndResolveRecipient(name);
return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
}
private boolean trustIdentity(
RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
) {
var identity = account.getIdentityKeyStore().getIdentity(recipientId);
if (identity == null) {
return false;
}
for (var id : ids) {
if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
account.getSignalProtocolStore()
.setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
} catch (IOException | UntrustedIdentityException e) {
logger.warn("Failed to send verification sync message: {}", e.getMessage());
}
}
if (!verifier.apply(identity.getIdentityKey())) {
return false;
}
account.save();
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
try {
var address = account.getRecipientStore().resolveServiceAddress(recipientId);
sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
} catch (IOException | UntrustedIdentityException e) {
logger.warn("Failed to send verification sync message: {}", e.getMessage());
}
return true;
}
@ -2499,6 +2429,7 @@ public class Manager implements Closeable {
theirIdentityKey);
}
@Deprecated
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
var canonicalizedNumber = UuidUtil.isUuid(identifier)
? identifier
@ -2506,12 +2437,14 @@ public class Manager implements Closeable {
return resolveSignalServiceAddress(canonicalizedNumber);
}
@Deprecated
public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
return resolveSignalServiceAddress(address);
}
@Deprecated
public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
if (address.matches(account.getSelfAddress())) {
return account.getSelfAddress();
@ -2520,6 +2453,27 @@ public class Manager implements Closeable {
return account.getRecipientStore().resolveServiceAddress(address);
}
public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
return account.getRecipientStore().resolveServiceAddress(recipientId);
}
public RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException {
var canonicalizedNumber = UuidUtil.isUuid(identifier)
? identifier
: PhoneNumberFormatter.formatNumber(identifier, account.getUsername());
var address = Utils.getSignalServiceAddressFromIdentifier(canonicalizedNumber);
return resolveRecipient(address);
}
public RecipientId resolveRecipient(SignalServiceAddress address) {
return account.getRecipientStore().resolveRecipientUntrusted(address);
}
private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
return account.getRecipientStore().resolveRecipient(address);
}
@Override
public void close() throws IOException {
close(true);

View file

@ -40,6 +40,7 @@ import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.Locale;
public class RegistrationManager implements Closeable {
@ -164,10 +165,10 @@ public class RegistrationManager implements Closeable {
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
account.setRegistrationLockPin(pin);
account.getSessionStore().archiveAllSessions();
account.getSignalProtocolStore()
.saveIdentity(account.getSelfAddress(),
account.getIdentityKeyPair().getPublicKey(),
TrustLevel.TRUSTED_VERIFIED);
final var recipientId = account.getRecipientStore().resolveRecipient(account.getSelfAddress());
final var publicKey = account.getIdentityKeyPair().getPublicKey();
account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
Manager m = null;
try {

View file

@ -14,12 +14,13 @@ import org.asamk.signal.manager.storage.contacts.ContactInfo;
import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
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;
import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientStore;
@ -81,10 +82,11 @@ public class SignalAccount implements Closeable {
private boolean registered = false;
private JsonSignalProtocolStore signalProtocolStore;
private SignalProtocolStore signalProtocolStore;
private PreKeyStore preKeyStore;
private SignedPreKeyStore signedPreKeyStore;
private SessionStore sessionStore;
private IdentityKeyStore identityKeyStore;
private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
private RecipientStore recipientStore;
@ -141,11 +143,14 @@ public class SignalAccount implements Closeable {
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.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
account.recipientStore::resolveRecipient,
identityKey,
registrationId);
account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
account.signedPreKeyStore,
account.sessionStore);
account.sessionStore,
account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
@ -190,11 +195,14 @@ public class SignalAccount implements Closeable {
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.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
account.recipientStore::resolveRecipient,
identityKey,
registrationId);
account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
account.signedPreKeyStore,
account.sessionStore);
account.sessionStore,
account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
@ -235,6 +243,7 @@ public class SignalAccount implements Closeable {
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
}
public static File getFileName(File dataPath, String username) {
@ -261,6 +270,10 @@ public class SignalAccount implements Closeable {
return new File(getUserPath(dataPath, username), "signed-pre-keys");
}
private static File getIdentitiesPath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "identities");
}
private static File getSessionsPath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "sessions");
}
@ -299,6 +312,17 @@ public class SignalAccount implements Closeable {
}
username = Utils.getNotNullNode(rootNode, "username").asText();
password = Utils.getNotNullNode(rootNode, "password").asText();
int registrationId = 0;
if (rootNode.hasNonNull("registrationId")) {
registrationId = rootNode.get("registrationId").asInt();
}
IdentityKeyPair identityKeyPair = null;
if (rootNode.hasNonNull("identityPrivateKey") && rootNode.hasNonNull("identityKey")) {
final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("identityKey").asText());
final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("identityPrivateKey").asText());
identityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
}
if (rootNode.hasNonNull("registrationLockPin")) {
registrationLockPin = rootNode.get("registrationLockPin").asText();
}
@ -338,13 +362,15 @@ public class SignalAccount implements Closeable {
}
}
signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
JsonSignalProtocolStore.class);
var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
LegacyJsonSignalProtocolStore.class)
: null;
preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
if (signalProtocolStore.getLegacyPreKeyStore() != null) {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
logger.debug("Migrating legacy pre key store.");
for (var entry : signalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
try {
preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
} catch (IOException e) {
@ -352,12 +378,11 @@ public class SignalAccount implements Closeable {
}
}
}
signalProtocolStore.setPreKeyStore(preKeyStore);
signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
if (signalProtocolStore.getLegacySignedPreKeyStore() != null) {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
logger.debug("Migrating legacy signed pre key store.");
for (var entry : signalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
try {
signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
} catch (IOException e) {
@ -365,12 +390,11 @@ public class SignalAccount implements Closeable {
}
}
}
signalProtocolStore.setSignedPreKeyStore(signedPreKeyStore);
sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
if (signalProtocolStore.getLegacySessionStore() != null) {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
logger.debug("Migrating legacy session store.");
for (var session : signalProtocolStore.getLegacySessionStore().getSessions()) {
for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
try {
sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
session.deviceId), new SessionRecord(session.sessionRecord));
@ -379,7 +403,27 @@ public class SignalAccount implements Closeable {
}
}
}
signalProtocolStore.setSessionStore(sessionStore);
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
identityKeyPair = legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentityKeyPair();
registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
}
identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
recipientStore::resolveRecipient,
identityKeyPair,
registrationId);
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
logger.debug("Migrating identity session store.");
for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
RecipientId recipientId = recipientStore.resolveRecipient(identity.getAddress());
identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
identityKeyStore.setIdentityTrustLevel(recipientId,
identity.getIdentityKey(),
identity.getTrustLevel());
}
}
signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
var groupStoreNode = rootNode.get("groupStore");
@ -431,10 +475,6 @@ public class SignalAccount implements Closeable {
.collect(Collectors.toSet());
}
}
for (var identity : signalProtocolStore.getIdentities()) {
identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
}
}
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@ -475,6 +515,13 @@ public class SignalAccount implements Closeable {
.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()))
@ -484,7 +531,6 @@ public class SignalAccount implements Closeable {
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
.putPOJO("profileStore", profileStore)
@ -517,10 +563,6 @@ public class SignalAccount implements Closeable {
return new Pair<>(fileChannel, lock);
}
public void setResolver(final SignalServiceAddressResolver resolver) {
signalProtocolStore.setResolver(resolver);
}
public void addPreKeys(List<PreKeyRecord> records) {
for (var record : records) {
if (preKeyIdOffset != record.getId()) {
@ -543,7 +585,7 @@ public class SignalAccount implements Closeable {
save();
}
public JsonSignalProtocolStore getSignalProtocolStore() {
public SignalProtocolStore getSignalProtocolStore() {
return signalProtocolStore;
}
@ -551,6 +593,10 @@ public class SignalAccount implements Closeable {
return sessionStore;
}
public IdentityKeyStore getIdentityKeyStore() {
return identityKeyStore;
}
public JsonGroupStore getGroupStore() {
return groupStore;
}

View file

@ -0,0 +1,48 @@
package org.asamk.signal.manager.storage.identities;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.libsignal.IdentityKey;
import java.util.Date;
public class IdentityInfo {
private final RecipientId recipientId;
private final IdentityKey identityKey;
private final TrustLevel trustLevel;
private final Date added;
IdentityInfo(
final RecipientId recipientId, IdentityKey identityKey, TrustLevel trustLevel, Date added
) {
this.recipientId = recipientId;
this.identityKey = identityKey;
this.trustLevel = trustLevel;
this.added = added;
}
public RecipientId getRecipientId() {
return recipientId;
}
public IdentityKey getIdentityKey() {
return this.identityKey;
}
public TrustLevel getTrustLevel() {
return this.trustLevel;
}
boolean isTrusted() {
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
}
public Date getDateAdded() {
return this.added;
}
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
}

View file

@ -0,0 +1,273 @@
package org.asamk.signal.manager.storage.identities;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.TrustLevel;
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.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class IdentityKeyStore implements org.whispersystems.libsignal.state.IdentityKeyStore {
private final static Logger logger = LoggerFactory.getLogger(IdentityKeyStore.class);
private final ObjectMapper objectMapper = org.asamk.signal.manager.storage.Utils.createStorageObjectMapper();
private final Map<RecipientId, IdentityInfo> cachedIdentities = new HashMap<>();
private final File identitiesPath;
private final RecipientResolver resolver;
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
public IdentityKeyStore(
final File identitiesPath,
final RecipientResolver resolver,
final IdentityKeyPair identityKeyPair,
final int localRegistrationId
) {
this.identitiesPath = identitiesPath;
this.resolver = resolver;
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId;
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyPair;
}
@Override
public int getLocalRegistrationId() {
return localRegistrationId;
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
final var recipientId = resolveRecipient(address.getName());
return saveIdentity(recipientId, identityKey, new Date());
}
public boolean saveIdentity(final RecipientId recipientId, final IdentityKey identityKey, Date added) {
synchronized (cachedIdentities) {
final var identityInfo = loadIdentityLocked(recipientId);
if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) {
// Identity already exists, not updating the trust level
return false;
}
final var trustLevel = identityInfo == null ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED;
final var newIdentityInfo = new IdentityInfo(recipientId, identityKey, trustLevel, added);
storeIdentityLocked(recipientId, newIdentityInfo);
return true;
}
}
public boolean setIdentityTrustLevel(
RecipientId recipientId, IdentityKey identityKey, TrustLevel trustLevel
) {
synchronized (cachedIdentities) {
final var identityInfo = loadIdentityLocked(recipientId);
if (identityInfo == null || !identityInfo.getIdentityKey().equals(identityKey)) {
// Identity not found, not updating the trust level
return false;
}
final var newIdentityInfo = new IdentityInfo(recipientId,
identityKey,
trustLevel,
identityInfo.getDateAdded());
storeIdentityLocked(recipientId, newIdentityInfo);
return true;
}
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
var recipientId = resolveRecipient(address.getName());
synchronized (cachedIdentities) {
final var identityInfo = loadIdentityLocked(recipientId);
if (identityInfo == null) {
// Identity not found
return true;
}
// TODO implement possibility for different handling of incoming/outgoing trust decisions
if (!identityInfo.getIdentityKey().equals(identityKey)) {
// Identity found, but different
return false;
}
return identityInfo.isTrusted();
}
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
var recipientId = resolveRecipient(address.getName());
synchronized (cachedIdentities) {
var identity = loadIdentityLocked(recipientId);
return identity == null ? null : identity.getIdentityKey();
}
}
public IdentityInfo getIdentity(RecipientId recipientId) {
synchronized (cachedIdentities) {
return loadIdentityLocked(recipientId);
}
}
final Pattern identityFileNamePattern = Pattern.compile("([0-9]+)");
public List<IdentityInfo> getIdentities() {
final var files = identitiesPath.listFiles();
if (files == null) {
return List.of();
}
return Arrays.stream(files)
.filter(f -> identityFileNamePattern.matcher(f.getName()).matches())
.map(f -> RecipientId.of(Integer.parseInt(f.getName())))
.map(this::loadIdentityLocked)
.collect(Collectors.toList());
}
public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
synchronized (cachedIdentities) {
deleteIdentityLocked(toBeMergedRecipientId);
}
}
/**
* @param identifier can be either a serialized uuid or a e164 phone number
*/
private RecipientId resolveRecipient(String identifier) {
return resolver.resolveRecipient(Utils.getSignalServiceAddressFromIdentifier(identifier));
}
private File getIdentityFile(final RecipientId recipientId) {
try {
IOUtils.createPrivateDirectories(identitiesPath);
} catch (IOException e) {
throw new AssertionError("Failed to create identities path", e);
}
return new File(identitiesPath, String.valueOf(recipientId.getId()));
}
private IdentityInfo loadIdentityLocked(final RecipientId recipientId) {
{
final var session = cachedIdentities.get(recipientId);
if (session != null) {
return session;
}
}
final var file = getIdentityFile(recipientId);
if (!file.exists()) {
return null;
}
try (var inputStream = new FileInputStream(file)) {
var storage = objectMapper.readValue(inputStream, IdentityStorage.class);
var id = new IdentityKey(Base64.getDecoder().decode(storage.getIdentityKey()));
var trustLevel = TrustLevel.fromInt(storage.getTrustLevel());
var added = new Date(storage.getAddedTimestamp());
final var identityInfo = new IdentityInfo(recipientId, id, trustLevel, added);
cachedIdentities.put(recipientId, identityInfo);
return identityInfo;
} catch (IOException | InvalidKeyException e) {
logger.warn("Failed to load identity key: {}", e.getMessage());
return null;
}
}
private void storeIdentityLocked(final RecipientId recipientId, final IdentityInfo identityInfo) {
cachedIdentities.put(recipientId, identityInfo);
var storage = new IdentityStorage(Base64.getEncoder().encodeToString(identityInfo.getIdentityKey().serialize()),
identityInfo.getTrustLevel().ordinal(),
identityInfo.getDateAdded().getTime());
final var file = getIdentityFile(recipientId);
// 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 identity file: {}", e.getMessage());
}
}
private void deleteIdentityLocked(final RecipientId recipientId) {
cachedIdentities.remove(recipientId);
final var file = getIdentityFile(recipientId);
if (!file.exists()) {
return;
}
try {
Files.delete(file.toPath());
} catch (IOException e) {
logger.error("Failed to delete identity file {}: {}", file, e.getMessage());
}
}
private static final class IdentityStorage {
private String identityKey;
private int trustLevel;
private long addedTimestamp;
// For deserialization
private IdentityStorage() {
}
private IdentityStorage(final String identityKey, final int trustLevel, final long addedTimestamp) {
this.identityKey = identityKey;
this.trustLevel = trustLevel;
this.addedTimestamp = addedTimestamp;
}
public String getIdentityKey() {
return identityKey;
}
public int getTrustLevel() {
return trustLevel;
}
public long getAddedTimestamp() {
return addedTimestamp;
}
}
}

View file

@ -1,277 +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.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
public class JsonIdentityKeyStore implements IdentityKeyStore {
private final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
private final List<IdentityInfo> identities = new ArrayList<>();
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
private SignalServiceAddressResolver resolver;
public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId;
}
public void setResolver(final SignalServiceAddressResolver resolver) {
this.resolver = resolver;
}
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Utils.getSignalServiceAddressFromIdentifier(identifier);
}
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyPair;
}
@Override
public int getLocalRegistrationId() {
return localRegistrationId;
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(resolveSignalServiceAddress(address.getName()),
identityKey,
TrustLevel.TRUSTED_UNVERIFIED,
null);
}
/**
* Adds the given identityKey for the user name and sets the trustLevel and added timestamp.
* If the identityKey already exists, the trustLevel and added timestamp are NOT updated.
*
* @param serviceAddress User address, i.e. phone number and/or uuid
* @param identityKey The user's public key
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
* @param added Added timestamp, if null and the key is newly added, the current time is used.
*/
public boolean saveIdentity(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added
) {
for (var id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
id.address = serviceAddress;
}
// Identity already exists, not updating the trust level
return true;
}
identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
return false;
}
/**
* Update trustLevel for the given identityKey for the user name.
*
* @param serviceAddress User address, i.e. phone number and/or uuid
* @param identityKey The user's public key
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
*/
public void setIdentityTrustLevel(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
) {
for (var id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
id.address = serviceAddress;
}
id.trustLevel = trustLevel;
return;
}
identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, new Date()));
}
public void removeIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey) {
identities.removeIf(id -> id.address.matches(serviceAddress) && id.identityKey.equals(identityKey));
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
// TODO implement possibility for different handling of incoming/outgoing trust decisions
var serviceAddress = resolveSignalServiceAddress(address.getName());
var trustOnFirstUse = true;
for (var id : identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
if (id.identityKey.equals(identityKey)) {
return id.isTrusted();
} else {
trustOnFirstUse = false;
}
}
if (!trustOnFirstUse) {
saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.UNTRUSTED, null);
}
return trustOnFirstUse;
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
var serviceAddress = resolveSignalServiceAddress(address.getName());
var identity = getIdentity(serviceAddress);
return identity == null ? null : identity.getIdentityKey();
}
public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
long maxDate = 0;
IdentityInfo maxIdentity = null;
for (var id : this.identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
final var time = id.getDateAdded().getTime();
if (maxIdentity == null || maxDate <= time) {
maxDate = time;
maxIdentity = id;
}
}
return maxIdentity;
}
public List<IdentityInfo> getIdentities() {
// TODO deep copy
return identities;
}
public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
var identities = new ArrayList<IdentityInfo>();
for (var identity : this.identities) {
if (identity.address.matches(serviceAddress)) {
identities.add(identity);
}
}
return identities;
}
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
@Override
public JsonIdentityKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
var localRegistrationId = node.get("registrationId").asInt();
var identityKeyPair = new IdentityKeyPair(Base64.getDecoder().decode(node.get("identityKey").asText()));
var keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId);
var trustedKeysNode = node.get("trustedKeys");
if (trustedKeysNode.isArray()) {
for (var trustedKey : trustedKeysNode) {
var trustedKeyName = trustedKey.hasNonNull("name") ? trustedKey.get("name").asText() : null;
if (UuidUtil.isUuid(trustedKeyName)) {
// Ignore identities that were incorrectly created with UUIDs as name
continue;
}
var uuid = trustedKey.hasNonNull("uuid")
? UuidUtil.parseOrNull(trustedKey.get("uuid").asText())
: null;
final var serviceAddress = uuid == null
? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName)
: new SignalServiceAddress(uuid, trustedKeyName);
try {
var id = new IdentityKey(Base64.getDecoder().decode(trustedKey.get("identityKey").asText()), 0);
var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
"trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
.asLong()) : new Date();
keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
} catch (InvalidKeyException e) {
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
}
}
}
return keyStore;
}
}
public static class JsonIdentityKeyStoreSerializer extends JsonSerializer<JsonIdentityKeyStore> {
@Override
public void serialize(
JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartObject();
json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
json.writeStringField("identityKey",
Base64.getEncoder().encodeToString(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeStringField("identityPrivateKey",
Base64.getEncoder()
.encodeToString(jsonIdentityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()));
json.writeStringField("identityPublicKey",
Base64.getEncoder()
.encodeToString(jsonIdentityKeyStore.getIdentityKeyPair().getPublicKey().serialize()));
json.writeArrayFieldStart("trustedKeys");
for (var trustedKey : jsonIdentityKeyStore.identities) {
json.writeStartObject();
if (trustedKey.getAddress().getNumber().isPresent()) {
json.writeStringField("name", trustedKey.getAddress().getNumber().get());
}
if (trustedKey.getAddress().getUuid().isPresent()) {
json.writeStringField("uuid", trustedKey.getAddress().getUuid().get().toString());
}
json.writeStringField("identityKey",
Base64.getEncoder().encodeToString(trustedKey.identityKey.serialize()));
json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal());
json.writeNumberField("addedTimestamp", trustedKey.added.getTime());
json.writeEndObject();
}
json.writeEndArray();
json.writeEndObject();
}
}
}

View file

@ -6,14 +6,14 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Date;
public class IdentityInfo {
public class LegacyIdentityInfo {
SignalServiceAddress address;
IdentityKey identityKey;
TrustLevel trustLevel;
Date added;
IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
LegacyIdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
this.address = address;
this.identityKey = identityKey;
this.trustLevel = trustLevel;

View file

@ -0,0 +1,120 @@
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 org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
public class LegacyJsonIdentityKeyStore {
private final static Logger logger = LoggerFactory.getLogger(LegacyJsonIdentityKeyStore.class);
private final List<LegacyIdentityInfo> identities;
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
private LegacyJsonIdentityKeyStore(
final List<LegacyIdentityInfo> identities, IdentityKeyPair identityKeyPair, int localRegistrationId
) {
this.identities = identities;
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId;
}
public List<LegacyIdentityInfo> getIdentities() {
return identities.stream()
.map(LegacyIdentityInfo::getAddress)
.collect(Collectors.toSet())
.stream()
.map(this::getIdentity)
.collect(Collectors.toList());
}
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyPair;
}
public int getLocalRegistrationId() {
return localRegistrationId;
}
private LegacyIdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
long maxDate = 0;
LegacyIdentityInfo maxIdentity = null;
for (var id : this.identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
final var time = id.getDateAdded().getTime();
if (maxIdentity == null || maxDate <= time) {
maxDate = time;
maxIdentity = id;
}
}
return maxIdentity;
}
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<LegacyJsonIdentityKeyStore> {
@Override
public LegacyJsonIdentityKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
var localRegistrationId = node.get("registrationId").asInt();
var identityKeyPair = new IdentityKeyPair(Base64.getDecoder().decode(node.get("identityKey").asText()));
var identities = new ArrayList<LegacyIdentityInfo>();
var trustedKeysNode = node.get("trustedKeys");
if (trustedKeysNode.isArray()) {
for (var trustedKey : trustedKeysNode) {
var trustedKeyName = trustedKey.hasNonNull("name") ? trustedKey.get("name").asText() : null;
if (UuidUtil.isUuid(trustedKeyName)) {
// Ignore identities that were incorrectly created with UUIDs as name
continue;
}
var uuid = trustedKey.hasNonNull("uuid")
? UuidUtil.parseOrNull(trustedKey.get("uuid").asText())
: null;
final var serviceAddress = uuid == null
? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName)
: new SignalServiceAddress(uuid, trustedKeyName);
try {
var id = new IdentityKey(Base64.getDecoder().decode(trustedKey.get("identityKey").asText()), 0);
var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
"trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
.asLong()) : new Date();
identities.add(new LegacyIdentityInfo(serviceAddress, id, trustLevel, added));
} catch (InvalidKeyException e) {
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
}
}
}
return new LegacyJsonIdentityKeyStore(identities, identityKeyPair, localRegistrationId);
}
}
}

View file

@ -16,13 +16,13 @@ import java.util.List;
public class LegacyJsonSessionStore {
private final List<SessionInfo> sessions;
private final List<LegacySessionInfo> sessions;
private LegacyJsonSessionStore(final List<SessionInfo> sessions) {
private LegacyJsonSessionStore(final List<LegacySessionInfo> sessions) {
this.sessions = sessions;
}
public List<SessionInfo> getSessions() {
public List<LegacySessionInfo> getSessions() {
return sessions;
}
@ -34,7 +34,7 @@ public class LegacyJsonSessionStore {
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
var sessions = new ArrayList<SessionInfo>();
var sessions = new ArrayList<LegacySessionInfo>();
if (node.isArray()) {
for (var session : node) {
@ -50,7 +50,7 @@ public class LegacyJsonSessionStore {
: new SignalServiceAddress(uuid, sessionName);
final var deviceId = session.get("deviceId").asInt();
final var record = Base64.getDecoder().decode(session.get("record").asText());
var sessionInfo = new SessionInfo(serviceAddress, deviceId, record);
var sessionInfo = new LegacySessionInfo(serviceAddress, deviceId, record);
sessions.add(sessionInfo);
}
}

View file

@ -0,0 +1,42 @@
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
public class LegacyJsonSignalProtocolStore {
@JsonProperty("preKeys")
@JsonDeserialize(using = LegacyJsonPreKeyStore.JsonPreKeyStoreDeserializer.class)
private LegacyJsonPreKeyStore legacyPreKeyStore;
@JsonProperty("sessionStore")
@JsonDeserialize(using = LegacyJsonSessionStore.JsonSessionStoreDeserializer.class)
private LegacyJsonSessionStore legacySessionStore;
@JsonProperty("signedPreKeyStore")
@JsonDeserialize(using = LegacyJsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class)
private LegacyJsonSignedPreKeyStore legacySignedPreKeyStore;
@JsonProperty("identityKeyStore")
@JsonDeserialize(using = LegacyJsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class)
private LegacyJsonIdentityKeyStore legacyIdentityKeyStore;
private LegacyJsonSignalProtocolStore() {
}
public LegacyJsonPreKeyStore getLegacyPreKeyStore() {
return legacyPreKeyStore;
}
public LegacyJsonSignedPreKeyStore getLegacySignedPreKeyStore() {
return legacySignedPreKeyStore;
}
public LegacyJsonSessionStore getLegacySessionStore() {
return legacySessionStore;
}
public LegacyJsonIdentityKeyStore getLegacyIdentityKeyStore() {
return legacyIdentityKeyStore;
}
}

View file

@ -2,7 +2,7 @@ package org.asamk.signal.manager.storage.protocol;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SessionInfo {
public class LegacySessionInfo {
public SignalServiceAddress address;
@ -10,7 +10,7 @@ public class SessionInfo {
public byte[] sessionRecord;
public SessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) {
LegacySessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) {
this.address = address;
this.deviceId = deviceId;
this.sessionRecord = sessionRecord;

View file

@ -1,15 +1,10 @@
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.TrustLevel;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SessionRecord;
@ -17,76 +12,26 @@ 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", "signedPreKeyStore"}, allowSetters = true)
public class JsonSignalProtocolStore implements SignalServiceProtocolStore {
public class SignalProtocolStore implements SignalServiceProtocolStore {
@JsonProperty("preKeys")
@JsonDeserialize(using = LegacyJsonPreKeyStore.JsonPreKeyStoreDeserializer.class)
private LegacyJsonPreKeyStore legacyPreKeyStore;
private final PreKeyStore preKeyStore;
private final SignedPreKeyStore signedPreKeyStore;
private final SignalServiceSessionStore sessionStore;
private final IdentityKeyStore identityKeyStore;
@JsonProperty("sessionStore")
@JsonDeserialize(using = LegacyJsonSessionStore.JsonSessionStoreDeserializer.class)
private LegacyJsonSessionStore legacySessionStore;
@JsonProperty("signedPreKeyStore")
@JsonDeserialize(using = LegacyJsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class)
private LegacyJsonSignedPreKeyStore legacySignedPreKeyStore;
@JsonProperty("identityKeyStore")
@JsonDeserialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class)
@JsonSerialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreSerializer.class)
private JsonIdentityKeyStore identityKeyStore;
private PreKeyStore preKeyStore;
private SignedPreKeyStore signedPreKeyStore;
private SignalServiceSessionStore sessionStore;
public JsonSignalProtocolStore() {
}
public JsonSignalProtocolStore(
IdentityKeyPair identityKeyPair,
int registrationId,
PreKeyStore preKeyStore,
SignedPreKeyStore signedPreKeyStore,
SignalServiceSessionStore sessionStore
public SignalProtocolStore(
final PreKeyStore preKeyStore,
final SignedPreKeyStore signedPreKeyStore,
final SignalServiceSessionStore sessionStore,
final IdentityKeyStore identityKeyStore
) {
this.preKeyStore = preKeyStore;
this.signedPreKeyStore = signedPreKeyStore;
this.sessionStore = sessionStore;
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
}
public void setResolver(final SignalServiceAddressResolver resolver) {
identityKeyStore.setResolver(resolver);
}
public void setPreKeyStore(final PreKeyStore preKeyStore) {
this.preKeyStore = preKeyStore;
}
public void setSignedPreKeyStore(final SignedPreKeyStore signedPreKeyStore) {
this.signedPreKeyStore = signedPreKeyStore;
}
public void setSessionStore(final SignalServiceSessionStore sessionStore) {
this.sessionStore = sessionStore;
}
public LegacyJsonPreKeyStore getLegacyPreKeyStore() {
return legacyPreKeyStore;
}
public LegacyJsonSignedPreKeyStore getLegacySignedPreKeyStore() {
return legacySignedPreKeyStore;
}
public LegacyJsonSessionStore getLegacySessionStore() {
return legacySessionStore;
this.identityKeyStore = identityKeyStore;
}
@Override
@ -104,28 +49,6 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore {
return identityKeyStore.saveIdentity(address, identityKey);
}
public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null);
}
public void setIdentityTrustLevel(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
) {
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
}
public void removeIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey) {
identityKeyStore.removeIdentity(serviceAddress, identityKey);
}
public List<IdentityInfo> getIdentities() {
return identityKeyStore.getIdentities();
}
public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentities(serviceAddress);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
@ -136,10 +59,6 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore {
return identityKeyStore.getIdentity(address);
}
public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentity(serviceAddress);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);

View file

@ -1,13 +0,0 @@
package org.asamk.signal.manager.storage.protocol;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface SignalServiceAddressResolver {
/**
* Get a SignalServiceAddress with number and/or uuid from an identifier name.
*
* @param identifier can be either a serialized uuid or a e164 phone number
*/
SignalServiceAddress resolveSignalServiceAddress(String identifier);
}

View file

@ -229,14 +229,15 @@ public class RecipientStore {
}
private void save() {
var storage = new Storage(recipients.entrySet()
.stream()
.map(pair -> new Storage.Recipient(pair.getKey().getId(),
pair.getValue().getNumber().orNull(),
pair.getValue().getUuid().transform(UUID::toString).orNull()))
.collect(Collectors.toList()), lastId);
// Write to memory first to prevent corrupting the file in case of serialization errors
try (var inMemoryOutput = new ByteArrayOutputStream()) {
var storage = new Storage(recipients.entrySet()
.stream()
.map(pair -> new Storage.Recipient(pair.getKey().getId(),
pair.getValue().getNumber().orNull(),
pair.getValue().getUuid().transform(UUID::toString).orNull()))
.collect(Collectors.toList()), lastId);
objectMapper.writeValue(inMemoryOutput, storage);
var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());

View file

@ -199,7 +199,7 @@ public class SessionStore implements SignalServiceSessionStore {
.collect(Collectors.toList());
}
private File getSessionPath(Key key) {
private File getSessionFile(Key key) {
try {
IOUtils.createPrivateDirectories(sessionsPath);
} catch (IOException e) {
@ -216,7 +216,7 @@ public class SessionStore implements SignalServiceSessionStore {
}
}
final var file = getSessionPath(key);
final var file = getSessionFile(key);
if (!file.exists()) {
return null;
}
@ -233,7 +233,7 @@ public class SessionStore implements SignalServiceSessionStore {
private void storeSessionLocked(final Key key, final SessionRecord session) {
cachedSessions.put(key, session);
final var file = getSessionPath(key);
final var file = getSessionFile(key);
try {
try (var outputStream = new FileOutputStream(file)) {
outputStream.write(session.serialize());
@ -263,7 +263,7 @@ public class SessionStore implements SignalServiceSessionStore {
private void deleteSessionLocked(final Key key) {
cachedSessions.remove(key);
final var file = getSessionPath(key);
final var file = getSessionFile(key);
if (!file.exists()) {
return;
}

View file

@ -6,6 +6,7 @@ import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
@ -23,6 +24,17 @@ public class KeyUtils {
private KeyUtils() {
}
public static IdentityKeyPair getIdentityKeyPair(byte[] publicKeyBytes, byte[] privateKeyBytes) {
try {
IdentityKey publicKey = new IdentityKey(publicKeyBytes);
ECPrivateKey privateKey = Curve.decodePrivatePoint(privateKeyBytes);
return new IdentityKeyPair(publicKey, privateKey);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static IdentityKeyPair generateIdentityKeyPair() {
var djbKeyPair = Curve.generateKeyPair();
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());