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());

View file

@ -8,11 +8,12 @@ import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.List;
@ -22,9 +23,10 @@ public class ListIdentitiesCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(ListIdentitiesCommand.class);
private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) {
var digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
final SignalServiceAddress address = m.resolveSignalServiceAddress(theirId.getRecipientId());
var digits = Util.formatSafetyNumber(m.computeSafetyNumber(address, theirId.getIdentityKey()));
writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}",
theirId.getAddress().getNumber().orNull(),
address.getNumber().orNull(),
theirId.getTrustLevel(),
theirId.getDateAdded(),
Hex.toString(theirId.getFingerprint()),

View file

@ -29,7 +29,12 @@ public class TrustCommand implements LocalCommand {
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
var number = ns.getString("number");
if (ns.getBoolean("trust_all_known_keys")) {
var res = m.trustIdentityAllKeys(number);
boolean res;
try {
res = m.trustIdentityAllKeys(number);
} catch (InvalidNumberException e) {
throw new UserErrorException("Failed to parse recipient: " + e.getMessage());
}
if (!res) {
throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct.");
}

View file

@ -8,6 +8,7 @@ import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.util.guava.Optional;
@ -144,7 +145,11 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendMessageReaction(
final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final String recipient
final String emoji,
final boolean remove,
final String targetAuthor,
final long targetSentTimestamp,
final String recipient
) {
var recipients = new ArrayList<String>(1);
recipients.add(recipient);
@ -153,7 +158,11 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendMessageReaction(
final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final List<String> recipients
final String emoji,
final boolean remove,
final String targetAuthor,
final long targetSentTimestamp,
final List<String> recipients
) {
try {
final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
@ -210,10 +219,18 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendGroupMessageReaction(
final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final byte[] groupId
final String emoji,
final boolean remove,
final String targetAuthor,
final long targetSentTimestamp,
final byte[] groupId
) {
try {
final var results = m.sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, GroupId.unknownVersion(groupId));
final var results = m.sendGroupMessageReaction(emoji,
remove,
targetAuthor,
targetSentTimestamp,
GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
@ -366,8 +383,11 @@ public class DbusSignalImpl implements Signal {
// all numbers the system knows
@Override
public List<String> listNumbers() {
return Stream.concat(m.getIdentities().stream().map(i -> i.getAddress().getNumber().orNull()),
m.getContacts().stream().map(c -> c.number))
return Stream.concat(m.getIdentities()
.stream()
.map(IdentityInfo::getRecipientId)
.map(m::resolveSignalServiceAddress)
.map(a -> a.getNumber().orNull()), m.getContacts().stream().map(c -> c.number))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
@ -385,7 +405,8 @@ public class DbusSignalImpl implements Signal {
}
// Try profiles if no contact name was found
for (var identity : m.getIdentities()) {
final var address = identity.getAddress();
final var recipientId = identity.getRecipientId();
final var address = m.resolveSignalServiceAddress(recipientId);
var number = address.getNumber().orNull();
if (number != null) {
var profile = m.getRecipientProfile(address);