mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Refactor identity key store
This commit is contained in:
parent
afb22deada
commit
8a0c6cae15
19 changed files with 717 additions and 563 deletions
|
@ -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.GroupInfo;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
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.messageCache.CachedMessage;
|
||||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
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.storage.stickers.Sticker;
|
||||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
|
@ -234,8 +235,6 @@ public class Manager implements Closeable {
|
||||||
clientZkProfileOperations,
|
clientZkProfileOperations,
|
||||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||||
|
|
||||||
this.account.setResolver(this::resolveSignalServiceAddress);
|
|
||||||
|
|
||||||
this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
|
this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
|
||||||
account.getProfileStore()::getProfileKey,
|
account.getProfileStore()::getProfileKey,
|
||||||
this::getRecipientProfile,
|
this::getRecipientProfile,
|
||||||
|
@ -1223,17 +1222,7 @@ public class Manager implements Closeable {
|
||||||
|
|
||||||
private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
|
private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
|
||||||
var messageSender = createMessageSender();
|
var messageSender = createMessageSender();
|
||||||
try {
|
messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
|
||||||
messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
|
|
||||||
} catch (UntrustedIdentityException e) {
|
|
||||||
if (e.getIdentityKey() != null) {
|
|
||||||
account.getSignalProtocolStore()
|
|
||||||
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
|
|
||||||
e.getIdentityKey(),
|
|
||||||
TrustLevel.UNTRUSTED);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
|
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
|
||||||
|
@ -1303,22 +1292,8 @@ public class Manager implements Closeable {
|
||||||
unidentifiedAccessHelper.getAccessFor(recipients),
|
unidentifiedAccessHelper.getAccessFor(recipients),
|
||||||
isRecipientUpdate,
|
isRecipientUpdate,
|
||||||
message);
|
message);
|
||||||
for (var r : result) {
|
|
||||||
if (r.getIdentityFailure() != null) {
|
|
||||||
account.getSignalProtocolStore()
|
|
||||||
.saveIdentity(r.getAddress(),
|
|
||||||
r.getIdentityFailure().getIdentityKey(),
|
|
||||||
TrustLevel.UNTRUSTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Pair<>(timestamp, result);
|
return new Pair<>(timestamp, result);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (UntrustedIdentityException e) {
|
||||||
if (e.getIdentityKey() != null) {
|
|
||||||
account.getSignalProtocolStore()
|
|
||||||
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
|
|
||||||
e.getIdentityKey(),
|
|
||||||
TrustLevel.UNTRUSTED);
|
|
||||||
}
|
|
||||||
return new Pair<>(timestamp, List.of());
|
return new Pair<>(timestamp, List.of());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1388,12 +1363,6 @@ public class Manager implements Closeable {
|
||||||
false,
|
false,
|
||||||
System.currentTimeMillis() - startTime);
|
System.currentTimeMillis() - startTime);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (UntrustedIdentityException e) {
|
||||||
if (e.getIdentityKey() != null) {
|
|
||||||
account.getSignalProtocolStore()
|
|
||||||
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
|
|
||||||
e.getIdentityKey(),
|
|
||||||
TrustLevel.UNTRUSTED);
|
|
||||||
}
|
|
||||||
return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
|
return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1406,12 +1375,6 @@ public class Manager implements Closeable {
|
||||||
try {
|
try {
|
||||||
return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
|
return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (UntrustedIdentityException e) {
|
||||||
if (e.getIdentityKey() != null) {
|
|
||||||
account.getSignalProtocolStore()
|
|
||||||
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
|
|
||||||
e.getIdentityKey(),
|
|
||||||
TrustLevel.UNTRUSTED);
|
|
||||||
}
|
|
||||||
return SendMessageResult.identityFailure(address, e.getIdentityKey());
|
return SendMessageResult.identityFailure(address, e.getIdentityKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1424,15 +1387,7 @@ public class Manager implements Closeable {
|
||||||
return cipher.decrypt(envelope);
|
return cipher.decrypt(envelope);
|
||||||
} catch (ProtocolUntrustedIdentityException e) {
|
} catch (ProtocolUntrustedIdentityException e) {
|
||||||
if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
|
if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
|
||||||
var identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
|
throw (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 new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
|
@ -2004,8 +1959,8 @@ public class Manager implements Closeable {
|
||||||
}
|
}
|
||||||
if (c.getVerified().isPresent()) {
|
if (c.getVerified().isPresent()) {
|
||||||
final var verifiedMessage = c.getVerified().get();
|
final var verifiedMessage = c.getVerified().get();
|
||||||
account.getSignalProtocolStore()
|
account.getIdentityKeyStore()
|
||||||
.setIdentityTrustLevel(verifiedMessage.getDestination(),
|
.setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
|
||||||
verifiedMessage.getIdentityKey(),
|
verifiedMessage.getIdentityKey(),
|
||||||
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
|
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
|
||||||
}
|
}
|
||||||
|
@ -2040,8 +1995,8 @@ public class Manager implements Closeable {
|
||||||
}
|
}
|
||||||
if (syncMessage.getVerified().isPresent()) {
|
if (syncMessage.getVerified().isPresent()) {
|
||||||
final var verifiedMessage = syncMessage.getVerified().get();
|
final var verifiedMessage = syncMessage.getVerified().get();
|
||||||
account.getSignalProtocolStore()
|
account.getIdentityKeyStore()
|
||||||
.setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()),
|
.setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
|
||||||
verifiedMessage.getIdentityKey(),
|
verifiedMessage.getIdentityKey(),
|
||||||
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
|
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
|
||||||
}
|
}
|
||||||
|
@ -2283,7 +2238,8 @@ public class Manager implements Closeable {
|
||||||
var out = new DeviceContactsOutputStream(fos);
|
var out = new DeviceContactsOutputStream(fos);
|
||||||
for (var record : account.getContactStore().getContacts()) {
|
for (var record : account.getContactStore().getContacts()) {
|
||||||
VerifiedMessage verifiedMessage = null;
|
VerifiedMessage verifiedMessage = null;
|
||||||
var currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress());
|
var currentIdentity = account.getIdentityKeyStore()
|
||||||
|
.getIdentity(resolveRecipientTrusted(record.getAddress()));
|
||||||
if (currentIdentity != null) {
|
if (currentIdentity != null) {
|
||||||
verifiedMessage = new VerifiedMessage(record.getAddress(),
|
verifiedMessage = new VerifiedMessage(record.getAddress(),
|
||||||
currentIdentity.getIdentityKey(),
|
currentIdentity.getIdentityKey(),
|
||||||
|
@ -2395,11 +2351,12 @@ public class Manager implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<IdentityInfo> getIdentities() {
|
public List<IdentityInfo> getIdentities() {
|
||||||
return account.getSignalProtocolStore().getIdentities();
|
return account.getIdentityKeyStore().getIdentities();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<IdentityInfo> getIdentities(String number) throws InvalidNumberException {
|
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
|
* @param fingerprint Fingerprint
|
||||||
*/
|
*/
|
||||||
public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
|
public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
|
||||||
var address = canonicalizeAndResolveSignalServiceAddress(name);
|
var recipientId = canonicalizeAndResolveRecipient(name);
|
||||||
return trustIdentity(address, (identityKey) -> Arrays.equals(identityKey.serialize(), fingerprint));
|
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
|
* @param safetyNumber Safety number
|
||||||
*/
|
*/
|
||||||
public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
|
public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
|
||||||
var address = canonicalizeAndResolveSignalServiceAddress(name);
|
var recipientId = canonicalizeAndResolveRecipient(name);
|
||||||
return trustIdentity(address, (identityKey) -> safetyNumber.equals(computeSafetyNumber(address, identityKey)));
|
var address = account.getRecipientStore().resolveServiceAddress(recipientId);
|
||||||
}
|
return trustIdentity(recipientId,
|
||||||
|
identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
|
||||||
private boolean trustIdentity(SignalServiceAddress address, Function<IdentityKey, Boolean> verifier) {
|
TrustLevel.TRUSTED_VERIFIED);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2468,24 +2391,31 @@ public class Manager implements Closeable {
|
||||||
*
|
*
|
||||||
* @param name username of the identity
|
* @param name username of the identity
|
||||||
*/
|
*/
|
||||||
public boolean trustIdentityAllKeys(String name) {
|
public boolean trustIdentityAllKeys(String name) throws InvalidNumberException {
|
||||||
var address = resolveSignalServiceAddress(name);
|
var recipientId = canonicalizeAndResolveRecipient(name);
|
||||||
var ids = account.getSignalProtocolStore().getIdentities(address);
|
return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
|
||||||
if (ids == null) {
|
}
|
||||||
|
|
||||||
|
private boolean trustIdentity(
|
||||||
|
RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
|
||||||
|
) {
|
||||||
|
var identity = account.getIdentityKeyStore().getIdentity(recipientId);
|
||||||
|
if (identity == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (var id : ids) {
|
|
||||||
if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
|
if (!verifier.apply(identity.getIdentityKey())) {
|
||||||
account.getSignalProtocolStore()
|
return false;
|
||||||
.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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2499,6 +2429,7 @@ public class Manager implements Closeable {
|
||||||
theirIdentityKey);
|
theirIdentityKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
|
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
|
||||||
var canonicalizedNumber = UuidUtil.isUuid(identifier)
|
var canonicalizedNumber = UuidUtil.isUuid(identifier)
|
||||||
? identifier
|
? identifier
|
||||||
|
@ -2506,12 +2437,14 @@ public class Manager implements Closeable {
|
||||||
return resolveSignalServiceAddress(canonicalizedNumber);
|
return resolveSignalServiceAddress(canonicalizedNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
|
public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
|
||||||
var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
|
var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
|
||||||
|
|
||||||
return resolveSignalServiceAddress(address);
|
return resolveSignalServiceAddress(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
|
public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
|
||||||
if (address.matches(account.getSelfAddress())) {
|
if (address.matches(account.getSelfAddress())) {
|
||||||
return account.getSelfAddress();
|
return account.getSelfAddress();
|
||||||
|
@ -2520,6 +2453,27 @@ public class Manager implements Closeable {
|
||||||
return account.getRecipientStore().resolveServiceAddress(address);
|
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
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
close(true);
|
close(true);
|
||||||
|
|
|
@ -40,6 +40,7 @@ import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class RegistrationManager implements Closeable {
|
public class RegistrationManager implements Closeable {
|
||||||
|
@ -164,10 +165,10 @@ public class RegistrationManager implements Closeable {
|
||||||
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
|
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
account.getSessionStore().archiveAllSessions();
|
account.getSessionStore().archiveAllSessions();
|
||||||
account.getSignalProtocolStore()
|
final var recipientId = account.getRecipientStore().resolveRecipient(account.getSelfAddress());
|
||||||
.saveIdentity(account.getSelfAddress(),
|
final var publicKey = account.getIdentityKeyPair().getPublicKey();
|
||||||
account.getIdentityKeyPair().getPublicKey(),
|
account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
|
||||||
TrustLevel.TRUSTED_VERIFIED);
|
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
|
||||||
|
|
||||||
Manager m = null;
|
Manager m = null;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -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.contacts.JsonContactsStore;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||||
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
|
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.messageCache.MessageCache;
|
||||||
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
|
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
|
||||||
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
|
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
|
||||||
import org.asamk.signal.manager.storage.profiles.ProfileStore;
|
import org.asamk.signal.manager.storage.profiles.ProfileStore;
|
||||||
import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore;
|
import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
|
||||||
import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver;
|
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
|
||||||
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
|
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
||||||
|
@ -81,10 +82,11 @@ public class SignalAccount implements Closeable {
|
||||||
|
|
||||||
private boolean registered = false;
|
private boolean registered = false;
|
||||||
|
|
||||||
private JsonSignalProtocolStore signalProtocolStore;
|
private SignalProtocolStore signalProtocolStore;
|
||||||
private PreKeyStore preKeyStore;
|
private PreKeyStore preKeyStore;
|
||||||
private SignedPreKeyStore signedPreKeyStore;
|
private SignedPreKeyStore signedPreKeyStore;
|
||||||
private SessionStore sessionStore;
|
private SessionStore sessionStore;
|
||||||
|
private IdentityKeyStore identityKeyStore;
|
||||||
private JsonGroupStore groupStore;
|
private JsonGroupStore groupStore;
|
||||||
private JsonContactsStore contactStore;
|
private JsonContactsStore contactStore;
|
||||||
private RecipientStore recipientStore;
|
private RecipientStore recipientStore;
|
||||||
|
@ -141,11 +143,14 @@ public class SignalAccount implements Closeable {
|
||||||
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
||||||
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
|
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
|
||||||
account.recipientStore::resolveRecipient);
|
account.recipientStore::resolveRecipient);
|
||||||
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey,
|
account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
|
||||||
registrationId,
|
account.recipientStore::resolveRecipient,
|
||||||
account.preKeyStore,
|
identityKey,
|
||||||
|
registrationId);
|
||||||
|
account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
|
||||||
account.signedPreKeyStore,
|
account.signedPreKeyStore,
|
||||||
account.sessionStore);
|
account.sessionStore,
|
||||||
|
account.identityKeyStore);
|
||||||
account.profileStore = new ProfileStore();
|
account.profileStore = new ProfileStore();
|
||||||
account.stickerStore = new StickerStore();
|
account.stickerStore = new StickerStore();
|
||||||
|
|
||||||
|
@ -190,11 +195,14 @@ public class SignalAccount implements Closeable {
|
||||||
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
||||||
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
|
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
|
||||||
account.recipientStore::resolveRecipient);
|
account.recipientStore::resolveRecipient);
|
||||||
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey,
|
account.identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
|
||||||
registrationId,
|
account.recipientStore::resolveRecipient,
|
||||||
account.preKeyStore,
|
identityKey,
|
||||||
|
registrationId);
|
||||||
|
account.signalProtocolStore = new SignalProtocolStore(account.preKeyStore,
|
||||||
account.signedPreKeyStore,
|
account.signedPreKeyStore,
|
||||||
account.sessionStore);
|
account.sessionStore,
|
||||||
|
account.identityKeyStore);
|
||||||
account.profileStore = new ProfileStore();
|
account.profileStore = new ProfileStore();
|
||||||
account.stickerStore = new StickerStore();
|
account.stickerStore = new StickerStore();
|
||||||
|
|
||||||
|
@ -235,6 +243,7 @@ public class SignalAccount implements Closeable {
|
||||||
|
|
||||||
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||||
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||||
|
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getFileName(File dataPath, String username) {
|
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");
|
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) {
|
private static File getSessionsPath(File dataPath, String username) {
|
||||||
return new File(getUserPath(dataPath, username), "sessions");
|
return new File(getUserPath(dataPath, username), "sessions");
|
||||||
}
|
}
|
||||||
|
@ -299,6 +312,17 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
username = Utils.getNotNullNode(rootNode, "username").asText();
|
username = Utils.getNotNullNode(rootNode, "username").asText();
|
||||||
password = Utils.getNotNullNode(rootNode, "password").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")) {
|
if (rootNode.hasNonNull("registrationLockPin")) {
|
||||||
registrationLockPin = rootNode.get("registrationLockPin").asText();
|
registrationLockPin = rootNode.get("registrationLockPin").asText();
|
||||||
}
|
}
|
||||||
|
@ -338,13 +362,15 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
|
var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
|
||||||
JsonSignalProtocolStore.class);
|
? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
|
||||||
|
LegacyJsonSignalProtocolStore.class)
|
||||||
|
: null;
|
||||||
|
|
||||||
preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
|
preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
|
||||||
if (signalProtocolStore.getLegacyPreKeyStore() != null) {
|
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
|
||||||
logger.debug("Migrating legacy pre key store.");
|
logger.debug("Migrating legacy pre key store.");
|
||||||
for (var entry : signalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
|
for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
|
||||||
try {
|
try {
|
||||||
preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
|
preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -352,12 +378,11 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
signalProtocolStore.setPreKeyStore(preKeyStore);
|
|
||||||
|
|
||||||
signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
||||||
if (signalProtocolStore.getLegacySignedPreKeyStore() != null) {
|
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
|
||||||
logger.debug("Migrating legacy signed pre key store.");
|
logger.debug("Migrating legacy signed pre key store.");
|
||||||
for (var entry : signalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
|
for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
|
||||||
try {
|
try {
|
||||||
signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
|
signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -365,12 +390,11 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
signalProtocolStore.setSignedPreKeyStore(signedPreKeyStore);
|
|
||||||
|
|
||||||
sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
|
sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
|
||||||
if (signalProtocolStore.getLegacySessionStore() != null) {
|
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
|
||||||
logger.debug("Migrating legacy session store.");
|
logger.debug("Migrating legacy session store.");
|
||||||
for (var session : signalProtocolStore.getLegacySessionStore().getSessions()) {
|
for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
|
||||||
try {
|
try {
|
||||||
sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
|
sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
|
||||||
session.deviceId), new SessionRecord(session.sessionRecord));
|
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();
|
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
|
||||||
var groupStoreNode = rootNode.get("groupStore");
|
var groupStoreNode = rootNode.get("groupStore");
|
||||||
|
@ -431,10 +475,6 @@ public class SignalAccount implements Closeable {
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var identity : signalProtocolStore.getIdentities()) {
|
|
||||||
identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||||
|
@ -475,6 +515,13 @@ public class SignalAccount implements Closeable {
|
||||||
.put("deviceId", deviceId)
|
.put("deviceId", deviceId)
|
||||||
.put("isMultiDevice", isMultiDevice)
|
.put("isMultiDevice", isMultiDevice)
|
||||||
.put("password", password)
|
.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("registrationLockPin", registrationLockPin)
|
||||||
.put("pinMasterKey",
|
.put("pinMasterKey",
|
||||||
pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
|
pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
|
||||||
|
@ -484,7 +531,6 @@ public class SignalAccount implements Closeable {
|
||||||
.put("nextSignedPreKeyId", nextSignedPreKeyId)
|
.put("nextSignedPreKeyId", nextSignedPreKeyId)
|
||||||
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
|
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
|
||||||
.put("registered", registered)
|
.put("registered", registered)
|
||||||
.putPOJO("axolotlStore", signalProtocolStore)
|
|
||||||
.putPOJO("groupStore", groupStore)
|
.putPOJO("groupStore", groupStore)
|
||||||
.putPOJO("contactStore", contactStore)
|
.putPOJO("contactStore", contactStore)
|
||||||
.putPOJO("profileStore", profileStore)
|
.putPOJO("profileStore", profileStore)
|
||||||
|
@ -517,10 +563,6 @@ public class SignalAccount implements Closeable {
|
||||||
return new Pair<>(fileChannel, lock);
|
return new Pair<>(fileChannel, lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResolver(final SignalServiceAddressResolver resolver) {
|
|
||||||
signalProtocolStore.setResolver(resolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addPreKeys(List<PreKeyRecord> records) {
|
public void addPreKeys(List<PreKeyRecord> records) {
|
||||||
for (var record : records) {
|
for (var record : records) {
|
||||||
if (preKeyIdOffset != record.getId()) {
|
if (preKeyIdOffset != record.getId()) {
|
||||||
|
@ -543,7 +585,7 @@ public class SignalAccount implements Closeable {
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonSignalProtocolStore getSignalProtocolStore() {
|
public SignalProtocolStore getSignalProtocolStore() {
|
||||||
return signalProtocolStore;
|
return signalProtocolStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,6 +593,10 @@ public class SignalAccount implements Closeable {
|
||||||
return sessionStore;
|
return sessionStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IdentityKeyStore getIdentityKeyStore() {
|
||||||
|
return identityKeyStore;
|
||||||
|
}
|
||||||
|
|
||||||
public JsonGroupStore getGroupStore() {
|
public JsonGroupStore getGroupStore() {
|
||||||
return groupStore;
|
return groupStore;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,14 +6,14 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public class IdentityInfo {
|
public class LegacyIdentityInfo {
|
||||||
|
|
||||||
SignalServiceAddress address;
|
SignalServiceAddress address;
|
||||||
IdentityKey identityKey;
|
IdentityKey identityKey;
|
||||||
TrustLevel trustLevel;
|
TrustLevel trustLevel;
|
||||||
Date added;
|
Date added;
|
||||||
|
|
||||||
IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
LegacyIdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.identityKey = identityKey;
|
this.identityKey = identityKey;
|
||||||
this.trustLevel = trustLevel;
|
this.trustLevel = trustLevel;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,13 +16,13 @@ import java.util.List;
|
||||||
|
|
||||||
public class LegacyJsonSessionStore {
|
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;
|
this.sessions = sessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SessionInfo> getSessions() {
|
public List<LegacySessionInfo> getSessions() {
|
||||||
return sessions;
|
return sessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ public class LegacyJsonSessionStore {
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||||
|
|
||||||
var sessions = new ArrayList<SessionInfo>();
|
var sessions = new ArrayList<LegacySessionInfo>();
|
||||||
|
|
||||||
if (node.isArray()) {
|
if (node.isArray()) {
|
||||||
for (var session : node) {
|
for (var session : node) {
|
||||||
|
@ -50,7 +50,7 @@ public class LegacyJsonSessionStore {
|
||||||
: new SignalServiceAddress(uuid, sessionName);
|
: new SignalServiceAddress(uuid, sessionName);
|
||||||
final var deviceId = session.get("deviceId").asInt();
|
final var deviceId = session.get("deviceId").asInt();
|
||||||
final var record = Base64.getDecoder().decode(session.get("record").asText());
|
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);
|
sessions.add(sessionInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ package org.asamk.signal.manager.storage.protocol;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
public class SessionInfo {
|
public class LegacySessionInfo {
|
||||||
|
|
||||||
public SignalServiceAddress address;
|
public SignalServiceAddress address;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ public class SessionInfo {
|
||||||
|
|
||||||
public byte[] sessionRecord;
|
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.address = address;
|
||||||
this.deviceId = deviceId;
|
this.deviceId = deviceId;
|
||||||
this.sessionRecord = sessionRecord;
|
this.sessionRecord = sessionRecord;
|
|
@ -1,15 +1,10 @@
|
||||||
package org.asamk.signal.manager.storage.protocol;
|
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.IdentityKey;
|
||||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
|
import org.whispersystems.libsignal.state.IdentityKeyStore;
|
||||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
import org.whispersystems.libsignal.state.PreKeyStore;
|
import org.whispersystems.libsignal.state.PreKeyStore;
|
||||||
import org.whispersystems.libsignal.state.SessionRecord;
|
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.libsignal.state.SignedPreKeyStore;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceProtocolStore;
|
import org.whispersystems.signalservice.api.SignalServiceProtocolStore;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceSessionStore;
|
import org.whispersystems.signalservice.api.SignalServiceSessionStore;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@JsonIgnoreProperties(value = {"sessionStore", "preKeys", "signedPreKeyStore"}, allowSetters = true)
|
public class SignalProtocolStore implements SignalServiceProtocolStore {
|
||||||
public class JsonSignalProtocolStore implements SignalServiceProtocolStore {
|
|
||||||
|
|
||||||
@JsonProperty("preKeys")
|
private final PreKeyStore preKeyStore;
|
||||||
@JsonDeserialize(using = LegacyJsonPreKeyStore.JsonPreKeyStoreDeserializer.class)
|
private final SignedPreKeyStore signedPreKeyStore;
|
||||||
private LegacyJsonPreKeyStore legacyPreKeyStore;
|
private final SignalServiceSessionStore sessionStore;
|
||||||
|
private final IdentityKeyStore identityKeyStore;
|
||||||
|
|
||||||
@JsonProperty("sessionStore")
|
public SignalProtocolStore(
|
||||||
@JsonDeserialize(using = LegacyJsonSessionStore.JsonSessionStoreDeserializer.class)
|
final PreKeyStore preKeyStore,
|
||||||
private LegacyJsonSessionStore legacySessionStore;
|
final SignedPreKeyStore signedPreKeyStore,
|
||||||
|
final SignalServiceSessionStore sessionStore,
|
||||||
@JsonProperty("signedPreKeyStore")
|
final IdentityKeyStore identityKeyStore
|
||||||
@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
|
|
||||||
) {
|
) {
|
||||||
this.preKeyStore = preKeyStore;
|
this.preKeyStore = preKeyStore;
|
||||||
this.signedPreKeyStore = signedPreKeyStore;
|
this.signedPreKeyStore = signedPreKeyStore;
|
||||||
this.sessionStore = sessionStore;
|
this.sessionStore = sessionStore;
|
||||||
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
|
this.identityKeyStore = identityKeyStore;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -104,28 +49,6 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore {
|
||||||
return identityKeyStore.saveIdentity(address, identityKey);
|
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
|
@Override
|
||||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||||
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
|
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
|
||||||
|
@ -136,10 +59,6 @@ public class JsonSignalProtocolStore implements SignalServiceProtocolStore {
|
||||||
return identityKeyStore.getIdentity(address);
|
return identityKeyStore.getIdentity(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
|
|
||||||
return identityKeyStore.getIdentity(serviceAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||||
return preKeyStore.loadPreKey(preKeyId);
|
return preKeyStore.loadPreKey(preKeyId);
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -229,14 +229,15 @@ public class RecipientStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void save() {
|
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
|
// Write to memory first to prevent corrupting the file in case of serialization errors
|
||||||
try (var inMemoryOutput = new ByteArrayOutputStream()) {
|
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);
|
objectMapper.writeValue(inMemoryOutput, storage);
|
||||||
|
|
||||||
var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
|
var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
|
||||||
|
|
|
@ -199,7 +199,7 @@ public class SessionStore implements SignalServiceSessionStore {
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getSessionPath(Key key) {
|
private File getSessionFile(Key key) {
|
||||||
try {
|
try {
|
||||||
IOUtils.createPrivateDirectories(sessionsPath);
|
IOUtils.createPrivateDirectories(sessionsPath);
|
||||||
} catch (IOException e) {
|
} 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()) {
|
if (!file.exists()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -233,7 +233,7 @@ public class SessionStore implements SignalServiceSessionStore {
|
||||||
private void storeSessionLocked(final Key key, final SessionRecord session) {
|
private void storeSessionLocked(final Key key, final SessionRecord session) {
|
||||||
cachedSessions.put(key, session);
|
cachedSessions.put(key, session);
|
||||||
|
|
||||||
final var file = getSessionPath(key);
|
final var file = getSessionFile(key);
|
||||||
try {
|
try {
|
||||||
try (var outputStream = new FileOutputStream(file)) {
|
try (var outputStream = new FileOutputStream(file)) {
|
||||||
outputStream.write(session.serialize());
|
outputStream.write(session.serialize());
|
||||||
|
@ -263,7 +263,7 @@ public class SessionStore implements SignalServiceSessionStore {
|
||||||
private void deleteSessionLocked(final Key key) {
|
private void deleteSessionLocked(final Key key) {
|
||||||
cachedSessions.remove(key);
|
cachedSessions.remove(key);
|
||||||
|
|
||||||
final var file = getSessionPath(key);
|
final var file = getSessionFile(key);
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.whispersystems.libsignal.IdentityKey;
|
||||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
import org.whispersystems.libsignal.ecc.Curve;
|
import org.whispersystems.libsignal.ecc.Curve;
|
||||||
|
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
import org.whispersystems.libsignal.util.Medium;
|
import org.whispersystems.libsignal.util.Medium;
|
||||||
|
@ -23,6 +24,17 @@ public class KeyUtils {
|
||||||
private 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() {
|
public static IdentityKeyPair generateIdentityKeyPair() {
|
||||||
var djbKeyPair = Curve.generateKeyPair();
|
var djbKeyPair = Curve.generateKeyPair();
|
||||||
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
var djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
||||||
|
|
|
@ -8,11 +8,12 @@ import org.asamk.signal.PlainTextWriterImpl;
|
||||||
import org.asamk.signal.commands.exceptions.CommandException;
|
import org.asamk.signal.commands.exceptions.CommandException;
|
||||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
import org.asamk.signal.manager.Manager;
|
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.Hex;
|
||||||
import org.asamk.signal.util.Util;
|
import org.asamk.signal.util.Util;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -22,9 +23,10 @@ public class ListIdentitiesCommand implements LocalCommand {
|
||||||
private final static Logger logger = LoggerFactory.getLogger(ListIdentitiesCommand.class);
|
private final static Logger logger = LoggerFactory.getLogger(ListIdentitiesCommand.class);
|
||||||
|
|
||||||
private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) {
|
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: {}",
|
writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}",
|
||||||
theirId.getAddress().getNumber().orNull(),
|
address.getNumber().orNull(),
|
||||||
theirId.getTrustLevel(),
|
theirId.getTrustLevel(),
|
||||||
theirId.getDateAdded(),
|
theirId.getDateAdded(),
|
||||||
Hex.toString(theirId.getFingerprint()),
|
Hex.toString(theirId.getFingerprint()),
|
||||||
|
|
|
@ -29,7 +29,12 @@ public class TrustCommand implements LocalCommand {
|
||||||
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
|
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
|
||||||
var number = ns.getString("number");
|
var number = ns.getString("number");
|
||||||
if (ns.getBoolean("trust_all_known_keys")) {
|
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) {
|
if (!res) {
|
||||||
throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct.");
|
throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.asamk.signal.manager.groups.GroupId;
|
||||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||||
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||||
|
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||||
import org.asamk.signal.util.ErrorUtils;
|
import org.asamk.signal.util.ErrorUtils;
|
||||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
@ -144,7 +145,11 @@ public class DbusSignalImpl implements Signal {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long sendMessageReaction(
|
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);
|
var recipients = new ArrayList<String>(1);
|
||||||
recipients.add(recipient);
|
recipients.add(recipient);
|
||||||
|
@ -153,7 +158,11 @@ public class DbusSignalImpl implements Signal {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long sendMessageReaction(
|
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 {
|
try {
|
||||||
final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
|
final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
|
||||||
|
@ -210,10 +219,18 @@ public class DbusSignalImpl implements Signal {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long sendGroupMessageReaction(
|
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 {
|
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());
|
checkSendMessageResults(results.first(), results.second());
|
||||||
return results.first();
|
return results.first();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -366,8 +383,11 @@ public class DbusSignalImpl implements Signal {
|
||||||
// all numbers the system knows
|
// all numbers the system knows
|
||||||
@Override
|
@Override
|
||||||
public List<String> listNumbers() {
|
public List<String> listNumbers() {
|
||||||
return Stream.concat(m.getIdentities().stream().map(i -> i.getAddress().getNumber().orNull()),
|
return Stream.concat(m.getIdentities()
|
||||||
m.getContacts().stream().map(c -> c.number))
|
.stream()
|
||||||
|
.map(IdentityInfo::getRecipientId)
|
||||||
|
.map(m::resolveSignalServiceAddress)
|
||||||
|
.map(a -> a.getNumber().orNull()), m.getContacts().stream().map(c -> c.number))
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.distinct()
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
@ -385,7 +405,8 @@ public class DbusSignalImpl implements Signal {
|
||||||
}
|
}
|
||||||
// Try profiles if no contact name was found
|
// Try profiles if no contact name was found
|
||||||
for (var identity : m.getIdentities()) {
|
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();
|
var number = address.getNumber().orNull();
|
||||||
if (number != null) {
|
if (number != null) {
|
||||||
var profile = m.getRecipientProfile(address);
|
var profile = m.getRecipientProfile(address);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue