mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Update libsignal-service-java
- Use session based number verification and registration
This commit is contained in:
parent
20b3563f21
commit
276ecef300
19 changed files with 359 additions and 158 deletions
|
@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
|
|||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
@ -12,7 +13,7 @@ public interface RegistrationManager extends Closeable {
|
|||
|
||||
void register(
|
||||
boolean voiceVerification, String captcha
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException;
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException;
|
||||
|
||||
void verifyAccount(
|
||||
String verificationCode, String pin
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
|
|||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.UpdateProfile;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
|
@ -27,6 +28,7 @@ import org.asamk.signal.manager.helper.AccountFileUpdater;
|
|||
import org.asamk.signal.manager.helper.PinHelper;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
@ -35,15 +37,13 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
|||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
|
||||
|
||||
class RegistrationManagerImpl implements RegistrationManager {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class);
|
||||
|
@ -106,7 +106,7 @@ class RegistrationManagerImpl implements RegistrationManager {
|
|||
@Override
|
||||
public void register(
|
||||
boolean voiceVerification, String captcha
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
|
||||
if (account.isRegistered()
|
||||
&& account.getServiceEnvironment() != null
|
||||
&& account.getServiceEnvironment() != serviceEnvironmentConfig.getType()) {
|
||||
|
@ -117,14 +117,21 @@ class RegistrationManagerImpl implements RegistrationManager {
|
|||
return;
|
||||
}
|
||||
|
||||
NumberVerificationUtils.requestVerificationCode(accountManager, captcha, voiceVerification);
|
||||
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
|
||||
account.getSessionId(account.getNumber()),
|
||||
id -> account.setSessionId(account.getNumber(), id),
|
||||
voiceVerification,
|
||||
captcha);
|
||||
NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyAccount(
|
||||
String verificationCode, String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException {
|
||||
final var result = NumberVerificationUtils.verifyNumber(verificationCode,
|
||||
var sessionId = account.getSessionId(account.getNumber());
|
||||
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
||||
verificationCode,
|
||||
pin,
|
||||
pinHelper,
|
||||
this::verifyAccountWithCode);
|
||||
|
@ -186,17 +193,7 @@ class RegistrationManagerImpl implements RegistrationManager {
|
|||
userAgent,
|
||||
null,
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
accountManager.setAccountAttributes(null,
|
||||
account.getLocalRegistrationId(),
|
||||
true,
|
||||
null,
|
||||
account.getRegistrationLock(),
|
||||
account.getSelfUnidentifiedAccessKey(),
|
||||
account.isUnrestrictedUnidentifiedAccess(),
|
||||
capabilities,
|
||||
account.isDiscoverableByPhoneNumber(),
|
||||
account.getEncryptedDeviceName(),
|
||||
account.getLocalPniRegistrationId());
|
||||
accountManager.setAccountAttributes(account.getAccountAttributes(null));
|
||||
account.setRegistered(true);
|
||||
logger.info("Reactivated existing account, verify is not necessary.");
|
||||
if (newManagerListener != null) {
|
||||
|
@ -215,29 +212,18 @@ class RegistrationManagerImpl implements RegistrationManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
private ServiceResponse<VerifyAccountResponse> verifyAccountWithCode(
|
||||
final String verificationCode, final String registrationLock
|
||||
) {
|
||||
if (registrationLock == null) {
|
||||
return accountManager.verifyAccount(verificationCode,
|
||||
account.getLocalRegistrationId(),
|
||||
true,
|
||||
account.getSelfUnidentifiedAccessKey(),
|
||||
account.isUnrestrictedUnidentifiedAccess(),
|
||||
ServiceConfig.capabilities,
|
||||
account.isDiscoverableByPhoneNumber(),
|
||||
account.getLocalPniRegistrationId());
|
||||
} else {
|
||||
return accountManager.verifyAccountWithRegistrationLockPin(verificationCode,
|
||||
account.getLocalRegistrationId(),
|
||||
true,
|
||||
registrationLock,
|
||||
account.getSelfUnidentifiedAccessKey(),
|
||||
account.isUnrestrictedUnidentifiedAccess(),
|
||||
ServiceConfig.capabilities,
|
||||
account.isDiscoverableByPhoneNumber(),
|
||||
account.getLocalPniRegistrationId());
|
||||
private VerifyAccountResponse verifyAccountWithCode(
|
||||
final String sessionId, final String verificationCode, final String registrationLock
|
||||
) throws IOException {
|
||||
try {
|
||||
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId));
|
||||
} catch (AlreadyVerifiedException e) {
|
||||
// Already verified so can continue registering
|
||||
}
|
||||
return Utils.handleResponseException(accountManager.registerAccount(sessionId,
|
||||
null,
|
||||
account.getAccountAttributes(registrationLock),
|
||||
true));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -88,6 +88,10 @@ public class SignalDependencies {
|
|||
return serviceEnvironmentConfig;
|
||||
}
|
||||
|
||||
public SignalSessionLock getSessionLock() {
|
||||
return sessionLock;
|
||||
}
|
||||
|
||||
public SignalServiceAccountManager getAccountManager() {
|
||||
return getOrCreate(() -> accountManager,
|
||||
() -> accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
|
||||
|
@ -115,7 +119,7 @@ public class SignalDependencies {
|
|||
|
||||
public GroupsV2Operations getGroupsV2Operations() {
|
||||
return getOrCreate(() -> groupsV2Operations,
|
||||
() -> groupsV2Operations = capabilities.isGv2()
|
||||
() -> groupsV2Operations = capabilities.getGv2()
|
||||
? new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()),
|
||||
ServiceConfig.GROUP_MAX_SIZE)
|
||||
: null);
|
||||
|
@ -123,7 +127,7 @@ public class SignalDependencies {
|
|||
|
||||
private ClientZkOperations getClientZkOperations() {
|
||||
return getOrCreate(() -> clientZkOperations,
|
||||
() -> clientZkOperations = capabilities.isGv2()
|
||||
() -> clientZkOperations = capabilities.getGv2()
|
||||
? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration())
|
||||
: null);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,13 @@ package org.asamk.signal.manager.api;
|
|||
|
||||
public class CaptchaRequiredException extends Exception {
|
||||
|
||||
private long nextAttemptTimestamp;
|
||||
|
||||
public CaptchaRequiredException(final long nextAttemptTimestamp) {
|
||||
super("Captcha required");
|
||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||
}
|
||||
|
||||
public CaptchaRequiredException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
@ -9,4 +16,8 @@ public class CaptchaRequiredException extends Exception {
|
|||
public CaptchaRequiredException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public long getNextAttemptTimestamp() {
|
||||
return nextAttemptTimestamp;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package org.asamk.signal.manager.api;
|
||||
|
||||
public class RateLimitException extends Exception {
|
||||
|
||||
private final long nextAttemptTimestamp;
|
||||
|
||||
public RateLimitException(final long nextAttemptTimestamp) {
|
||||
super("Rate limit");
|
||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
||||
}
|
||||
|
||||
public long getNextAttemptTimestamp() {
|
||||
return nextAttemptTimestamp;
|
||||
}
|
||||
}
|
|
@ -7,10 +7,11 @@ import org.asamk.signal.manager.api.IncorrectPinException;
|
|||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
|
@ -21,6 +22,7 @@ import org.whispersystems.signalservice.api.push.ACI;
|
|||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||
|
@ -128,9 +130,14 @@ public class AccountHelper {
|
|||
|
||||
public void startChangeNumber(
|
||||
String newNumber, String captcha, boolean voiceVerification
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
|
||||
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
|
||||
NumberVerificationUtils.requestVerificationCode(accountManager, captcha, voiceVerification);
|
||||
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
|
||||
account.getSessionId(newNumber),
|
||||
id -> account.setSessionId(newNumber, id),
|
||||
voiceVerification,
|
||||
captcha);
|
||||
NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
|
||||
}
|
||||
|
||||
public void finishChangeNumber(
|
||||
|
@ -140,17 +147,28 @@ public class AccountHelper {
|
|||
final List<OutgoingPushMessage> deviceMessages = null;
|
||||
final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
|
||||
final Map<String, Integer> pniRegistrationIds = null;
|
||||
final var result = NumberVerificationUtils.verifyNumber(verificationCode,
|
||||
var sessionId = account.getSessionId(account.getNumber());
|
||||
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
||||
verificationCode,
|
||||
pin,
|
||||
context.getPinHelper(),
|
||||
(verificationCode1, registrationLock) -> dependencies.getAccountManager()
|
||||
.changeNumber(new ChangePhoneNumberRequest(newNumber,
|
||||
verificationCode1,
|
||||
registrationLock,
|
||||
account.getPniIdentityKeyPair().getPublicKey(),
|
||||
deviceMessages,
|
||||
devicePniSignedPreKeys,
|
||||
pniRegistrationIds)));
|
||||
(sessionId1, verificationCode1, registrationLock) -> {
|
||||
final var accountManager = dependencies.getAccountManager();
|
||||
try {
|
||||
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
|
||||
} catch (AlreadyVerifiedException e) {
|
||||
// Already verified so can continue changing number
|
||||
}
|
||||
return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
|
||||
sessionId1,
|
||||
null,
|
||||
newNumber,
|
||||
registrationLock,
|
||||
account.getPniIdentityKeyPair().getPublicKey(),
|
||||
deviceMessages,
|
||||
devicePniSignedPreKeys,
|
||||
pniRegistrationIds)));
|
||||
});
|
||||
// TODO handle response
|
||||
updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
|
||||
}
|
||||
|
@ -162,18 +180,7 @@ public class AccountHelper {
|
|||
}
|
||||
|
||||
public void updateAccountAttributes() throws IOException {
|
||||
dependencies.getAccountManager()
|
||||
.setAccountAttributes(null,
|
||||
account.getLocalRegistrationId(),
|
||||
true,
|
||||
null,
|
||||
account.getRegistrationLock(),
|
||||
account.getSelfUnidentifiedAccessKey(),
|
||||
account.isUnrestrictedUnidentifiedAccess(),
|
||||
ServiceConfig.capabilities,
|
||||
account.isDiscoverableByPhoneNumber(),
|
||||
account.getEncryptedDeviceName(),
|
||||
account.getLocalPniRegistrationId());
|
||||
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
|
||||
}
|
||||
|
||||
public void addDevice(DeviceLinkInfo deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
|
||||
|
|
|
@ -80,7 +80,7 @@ public class IdentityHelper {
|
|||
final var address = account.getRecipientAddressResolver()
|
||||
.resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId));
|
||||
|
||||
return Utils.computeSafetyNumber(capabilities.isUuid(),
|
||||
return Utils.computeSafetyNumber(capabilities.getUuid(),
|
||||
account.getSelfRecipientAddress(),
|
||||
account.getAciIdentityKeyPair().getPublicKey(),
|
||||
address.getServiceId().equals(serviceId)
|
||||
|
|
|
@ -44,12 +44,14 @@ import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
|||
import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
|
@ -275,7 +277,8 @@ public final class IncomingMessageHandler {
|
|||
logger.debug("Received a sender key distribution message for distributionId {} from {}",
|
||||
message.getDistributionId(),
|
||||
protocolAddress);
|
||||
dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message);
|
||||
new SignalGroupSessionBuilder(dependencies.getSessionLock(),
|
||||
new GroupSessionBuilder(account.getSenderKeyStore())).process(protocolAddress, message);
|
||||
}
|
||||
|
||||
if (content.getDecryptionErrorMessage().isPresent()) {
|
||||
|
|
|
@ -144,22 +144,28 @@ public class ReceiveHelper {
|
|||
logger.debug("Checking for new message from server");
|
||||
try {
|
||||
isWaitingForMessage = true;
|
||||
var result = signalWebSocket.readOrEmpty(timeout.toMillis(), envelope1 -> {
|
||||
var queueNotEmpty = signalWebSocket.readMessageBatch(timeout.toMillis(), 1, batch -> {
|
||||
logger.debug("Retrieved {} envelopes!", batch.size());
|
||||
isWaitingForMessage = false;
|
||||
final var recipientId = envelope1.hasSourceUuid() ? account.getRecipientResolver()
|
||||
.resolveRecipient(envelope1.getSourceAddress()) : null;
|
||||
logger.trace("Storing new message from {}", recipientId);
|
||||
// store message on disk, before acknowledging receipt to the server
|
||||
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
||||
for (final var it : batch) {
|
||||
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
|
||||
it.getServerDeliveredTimestamp());
|
||||
final var recipientId = envelope1.hasSourceUuid() ? account.getRecipientResolver()
|
||||
.resolveRecipient(envelope1.getSourceAddress()) : null;
|
||||
logger.trace("Storing new message from {}", recipientId);
|
||||
// store message on disk, before acknowledging receipt to the server
|
||||
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
isWaitingForMessage = false;
|
||||
backOffCounter = 0;
|
||||
|
||||
if (result.isPresent()) {
|
||||
if (queueNotEmpty) {
|
||||
if (remainingMessages > 0) {
|
||||
remainingMessages -= 1;
|
||||
}
|
||||
envelope = result.get();
|
||||
envelope = cachedMessage[0].loadEnvelope();
|
||||
logger.debug("New message received from server");
|
||||
} else {
|
||||
logger.debug("Received indicator that server queue is empty");
|
||||
|
|
|
@ -62,6 +62,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
|
@ -97,6 +98,8 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
|
||||
|
||||
public class SignalAccount implements Closeable {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||
|
@ -119,6 +122,8 @@ public class SignalAccount implements Closeable {
|
|||
private String number;
|
||||
private ACI aci;
|
||||
private PNI pni;
|
||||
private String sessionId;
|
||||
private String sessionNumber;
|
||||
private String encryptedDeviceName;
|
||||
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
private boolean isMultiDevice = false;
|
||||
|
@ -551,6 +556,12 @@ public class SignalAccount implements Closeable {
|
|||
throw new IOException("Config file contains an invalid pni, needs to be a valid UUID", e);
|
||||
}
|
||||
}
|
||||
if (rootNode.hasNonNull("sessionId")) {
|
||||
sessionId = rootNode.get("sessionId").asText();
|
||||
}
|
||||
if (rootNode.hasNonNull("sessionNumber")) {
|
||||
sessionNumber = rootNode.get("sessionNumber").asText();
|
||||
}
|
||||
if (rootNode.hasNonNull("deviceName")) {
|
||||
encryptedDeviceName = rootNode.get("deviceName").asText();
|
||||
}
|
||||
|
@ -926,6 +937,8 @@ public class SignalAccount implements Closeable {
|
|||
.put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name())
|
||||
.put("uuid", aci == null ? null : aci.toString())
|
||||
.put("pni", pni == null ? null : pni.toString())
|
||||
.put("sessionId", sessionId)
|
||||
.put("sessionNumber", sessionNumber)
|
||||
.put("deviceName", encryptedDeviceName)
|
||||
.put("deviceId", deviceId)
|
||||
.put("isMultiDevice", isMultiDevice)
|
||||
|
@ -1293,6 +1306,21 @@ public class SignalAccount implements Closeable {
|
|||
save();
|
||||
}
|
||||
|
||||
public AccountAttributes getAccountAttributes(String registrationLock) {
|
||||
return new AccountAttributes(null,
|
||||
getLocalRegistrationId(),
|
||||
true,
|
||||
null,
|
||||
registrationLock != null ? registrationLock : getRegistrationLock(),
|
||||
getSelfUnidentifiedAccessKey(),
|
||||
isUnrestrictedUnidentifiedAccess(),
|
||||
capabilities,
|
||||
isDiscoverableByPhoneNumber(),
|
||||
encryptedDeviceName,
|
||||
getLocalPniRegistrationId(),
|
||||
null); // TODO recoveryPassword?
|
||||
}
|
||||
|
||||
public ServiceId getAccountId(ServiceIdType serviceIdType) {
|
||||
return serviceIdType.equals(ServiceIdType.ACI) ? aci : pni;
|
||||
}
|
||||
|
@ -1347,6 +1375,19 @@ public class SignalAccount implements Closeable {
|
|||
return getRecipientResolver().resolveRecipient(getSelfRecipientAddress());
|
||||
}
|
||||
|
||||
public String getSessionId(final String forNumber) {
|
||||
if (!forNumber.equals(sessionNumber)) {
|
||||
return null;
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(final String sessionNumber, final String sessionId) {
|
||||
this.sessionNumber = sessionNumber;
|
||||
this.sessionId = sessionId;
|
||||
save();
|
||||
}
|
||||
|
||||
public byte[] getEncryptedDeviceName() {
|
||||
return encryptedDeviceName == null ? null : Base64.getDecoder().decode(encryptedDeviceName);
|
||||
}
|
||||
|
|
|
@ -15,21 +15,30 @@ public final class CachedMessage {
|
|||
|
||||
private final File file;
|
||||
|
||||
private SignalServiceEnvelope envelope;
|
||||
|
||||
CachedMessage(final File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
CachedMessage(final File file, SignalServiceEnvelope envelope) {
|
||||
this.file = file;
|
||||
this.envelope = envelope;
|
||||
}
|
||||
|
||||
File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public SignalServiceEnvelope loadEnvelope() {
|
||||
try {
|
||||
return MessageCacheUtils.loadEnvelope(file);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage(), e);
|
||||
return null;
|
||||
if (envelope == null) {
|
||||
try {
|
||||
envelope = MessageCacheUtils.loadEnvelope(file);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return envelope;
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
|
|
|
@ -54,7 +54,7 @@ public class MessageCache {
|
|||
try {
|
||||
var cacheFile = getMessageCacheFile(recipientId, now, envelope.getTimestamp());
|
||||
MessageCacheUtils.storeEnvelope(envelope, cacheFile);
|
||||
return new CachedMessage(cacheFile);
|
||||
return new CachedMessage(cacheFile, envelope);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
|
|
|
@ -5,41 +5,94 @@ import org.asamk.signal.manager.api.IncorrectPinException;
|
|||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.helper.PinHelper;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class NumberVerificationUtils {
|
||||
|
||||
public static String handleVerificationSession(
|
||||
SignalServiceAccountManager accountManager,
|
||||
String sessionId,
|
||||
Consumer<String> sessionIdSaver,
|
||||
boolean voiceVerification,
|
||||
String captcha
|
||||
) throws CaptchaRequiredException, IOException, RateLimitException {
|
||||
RegistrationSessionMetadataResponse sessionResponse;
|
||||
try {
|
||||
sessionResponse = getValidSession(accountManager, sessionId);
|
||||
} catch (PushChallengeRequiredException |
|
||||
org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
|
||||
if (captcha != null) {
|
||||
sessionResponse = submitCaptcha(accountManager, sessionId, captcha);
|
||||
} else {
|
||||
throw new CaptchaRequiredException("Captcha Required");
|
||||
}
|
||||
}
|
||||
|
||||
sessionId = sessionResponse.getBody().getId();
|
||||
sessionIdSaver.accept(sessionId);
|
||||
|
||||
if (sessionResponse.getBody().getVerified()) {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
if (sessionResponse.getBody().getAllowedToRequestCode()) {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
final var nextAttempt = voiceVerification
|
||||
? sessionResponse.getBody().getNextCall()
|
||||
: sessionResponse.getBody().getNextSms();
|
||||
if (nextAttempt != null && nextAttempt > 0) {
|
||||
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextAttempt * 1000;
|
||||
throw new RateLimitException(timestamp);
|
||||
}
|
||||
|
||||
final var nextVerificationAttempt = sessionResponse.getBody().getNextVerificationAttempt();
|
||||
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
|
||||
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextVerificationAttempt * 1000;
|
||||
throw new CaptchaRequiredException(timestamp);
|
||||
}
|
||||
|
||||
if (sessionResponse.getBody().getRequestedInformation().contains("captcha")) {
|
||||
if (captcha != null) {
|
||||
sessionResponse = submitCaptcha(accountManager, sessionId, captcha);
|
||||
}
|
||||
if (!sessionResponse.getBody().getAllowedToRequestCode()) {
|
||||
throw new CaptchaRequiredException("Captcha Required");
|
||||
}
|
||||
}
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public static void requestVerificationCode(
|
||||
SignalServiceAccountManager accountManager, String captcha, boolean voiceVerification
|
||||
SignalServiceAccountManager accountManager, String sessionId, boolean voiceVerification
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
|
||||
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
|
||||
final ServiceResponse<RequestVerificationCodeResponse> response;
|
||||
final ServiceResponse<RegistrationSessionMetadataResponse> response;
|
||||
final var locale = Utils.getDefaultLocale(Locale.US);
|
||||
if (voiceVerification) {
|
||||
response = accountManager.requestVoiceVerificationCode(locale,
|
||||
Optional.ofNullable(captcha),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
response = accountManager.requestVoiceVerificationCode(sessionId, locale, false);
|
||||
} else {
|
||||
response = accountManager.requestSmsVerificationCode(locale,
|
||||
false,
|
||||
Optional.ofNullable(captcha),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
response = accountManager.requestSmsVerificationCode(sessionId, locale, false);
|
||||
}
|
||||
try {
|
||||
handleResponseException(response);
|
||||
Utils.handleResponseException(response);
|
||||
} catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
|
||||
throw new CaptchaRequiredException(e.getMessage(), e);
|
||||
} catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) {
|
||||
|
@ -51,11 +104,11 @@ public class NumberVerificationUtils {
|
|||
}
|
||||
|
||||
public static Pair<VerifyAccountResponse, MasterKey> verifyNumber(
|
||||
String verificationCode, String pin, PinHelper pinHelper, Verifier verifier
|
||||
String sessionId, String verificationCode, String pin, PinHelper pinHelper, Verifier verifier
|
||||
) throws IOException, PinLockedException, IncorrectPinException {
|
||||
verificationCode = verificationCode.replace("-", "");
|
||||
try {
|
||||
final var response = verifyAccountWithCode(verificationCode, null, verifier);
|
||||
final var response = verifier.verify(sessionId, verificationCode, null);
|
||||
|
||||
return new Pair<>(response, null);
|
||||
} catch (LockedException e) {
|
||||
|
@ -72,7 +125,7 @@ public class NumberVerificationUtils {
|
|||
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
|
||||
VerifyAccountResponse response;
|
||||
try {
|
||||
response = verifyAccountWithCode(verificationCode, registrationLock, verifier);
|
||||
response = verifier.verify(sessionId, verificationCode, registrationLock);
|
||||
} catch (LockedException _e) {
|
||||
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
|
||||
}
|
||||
|
@ -81,29 +134,53 @@ public class NumberVerificationUtils {
|
|||
}
|
||||
}
|
||||
|
||||
private static VerifyAccountResponse verifyAccountWithCode(
|
||||
final String verificationCode, final String registrationLock, final Verifier verifier
|
||||
private static RegistrationSessionMetadataResponse validateSession(
|
||||
final SignalServiceAccountManager accountManager, final String sessionId
|
||||
) throws IOException {
|
||||
final var response = verifier.verify(verificationCode, registrationLock);
|
||||
handleResponseException(response);
|
||||
return response.getResult().get();
|
||||
if (sessionId == null || sessionId.isEmpty()) {
|
||||
throw new NoSuchSessionException();
|
||||
}
|
||||
return Utils.handleResponseException(accountManager.getRegistrationSession(sessionId));
|
||||
}
|
||||
|
||||
private static void handleResponseException(final ServiceResponse<?> response) throws IOException {
|
||||
final var throwableOptional = response.getExecutionError().or(response::getApplicationError);
|
||||
if (throwableOptional.isPresent()) {
|
||||
if (throwableOptional.get() instanceof IOException) {
|
||||
throw (IOException) throwableOptional.get();
|
||||
} else {
|
||||
throw new IOException(throwableOptional.get());
|
||||
private static RegistrationSessionMetadataResponse requestValidSession(
|
||||
final SignalServiceAccountManager accountManager
|
||||
) throws NoSuchSessionException, IOException {
|
||||
return Utils.handleResponseException(accountManager.createRegistrationSession(null, "", ""));
|
||||
}
|
||||
|
||||
private static RegistrationSessionMetadataResponse getValidSession(
|
||||
final SignalServiceAccountManager accountManager, final String sessionId
|
||||
) throws IOException {
|
||||
try {
|
||||
return validateSession(accountManager, sessionId);
|
||||
} catch (NoSuchSessionException e) {
|
||||
return requestValidSession(accountManager);
|
||||
}
|
||||
}
|
||||
|
||||
private static RegistrationSessionMetadataResponse submitCaptcha(
|
||||
SignalServiceAccountManager accountManager, String sessionId, String captcha
|
||||
) throws IOException, CaptchaRequiredException {
|
||||
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
|
||||
try {
|
||||
return Utils.handleResponseException(accountManager.submitCaptchaToken(sessionId, captcha));
|
||||
} catch (PushChallengeRequiredException |
|
||||
org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException |
|
||||
TokenNotAcceptedException _e) {
|
||||
throw new CaptchaRequiredException("Captcha not accepted");
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.getCode() == 400) {
|
||||
throw new CaptchaRequiredException("Captcha has invalid format");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Verifier {
|
||||
|
||||
ServiceResponse<VerifyAccountResponse> verify(
|
||||
String verificationCode, String registrationLock
|
||||
);
|
||||
VerifyAccountResponse verify(
|
||||
String sessionId, String verificationCode, String registrationLock
|
||||
) throws IOException;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
|
@ -128,4 +129,16 @@ public class Utils {
|
|||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public static <T> T handleResponseException(final ServiceResponse<T> response) throws IOException {
|
||||
final var throwableOptional = response.getExecutionError().or(response::getApplicationError);
|
||||
if (throwableOptional.isPresent()) {
|
||||
if (throwableOptional.get() instanceof IOException) {
|
||||
throw (IOException) throwableOptional.get();
|
||||
} else {
|
||||
throw new IOException(throwableOptional.get());
|
||||
}
|
||||
}
|
||||
return response.getResult().orElse(null);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue