Update libsignal-service-java

- Use session based number verification and registration
This commit is contained in:
AsamK 2023-03-31 17:16:59 +02:00
parent 20b3563f21
commit 276ecef300
19 changed files with 359 additions and 158 deletions

View file

@ -76,6 +76,13 @@
{"name":"storeSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.state.SessionRecord"] } {"name":"storeSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.state.SessionRecord"] }
] ]
}, },
{
"name":"org.asamk.signal.manager.storage.senderKeys.SenderKeyStore",
"methods":[
{"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] },
{"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] }
]
},
{ {
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints", "name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }] "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]

View file

@ -78,22 +78,10 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true "allDeclaredMethods":true
}, },
{
"name":"com.google.protobuf.DescriptorMessageInfoFactory"
},
{
"name":"com.google.protobuf.ExtensionRegistry"
},
{
"name":"com.google.protobuf.ExtensionSchemaFull"
},
{ {
"name":"com.google.protobuf.GeneratedMessageLite", "name":"com.google.protobuf.GeneratedMessageLite",
"fields":[{"name":"unknownFields"}] "fields":[{"name":"unknownFields"}]
}, },
{
"name":"com.google.protobuf.GeneratedMessageV3"
},
{ {
"name":"com.google.protobuf.Internal$LongList", "name":"com.google.protobuf.Internal$LongList",
"allDeclaredMethods":true "allDeclaredMethods":true
@ -108,19 +96,10 @@
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true "allDeclaredConstructors":true
}, },
{
"name":"com.google.protobuf.MapFieldSchemaFull"
},
{
"name":"com.google.protobuf.NewInstanceSchemaFull"
},
{ {
"name":"com.google.protobuf.PrimitiveNonBoxingCollection", "name":"com.google.protobuf.PrimitiveNonBoxingCollection",
"allDeclaredMethods":true "allDeclaredMethods":true
}, },
{
"name":"com.google.protobuf.UnknownFieldSetSchema"
},
{ {
"name":"com.sun.crypto.provider.AESCipher$General", "name":"com.sun.crypto.provider.AESCipher$General",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
@ -465,9 +444,6 @@
{ {
"name":"javax.smartcardio.CardPermission" "name":"javax.smartcardio.CardPermission"
}, },
{
"name":"libcore.io.Memory"
},
{ {
"name":"long", "name":"long",
"allDeclaredMethods":true, "allDeclaredMethods":true,
@ -982,10 +958,6 @@
{"name":"startColor","parameterTypes":[] } {"name":"startColor","parameterTypes":[] }
] ]
}, },
{
"name":"org.asamk.signal.json.JsonStreamSerializer",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{ {
"name":"org.asamk.signal.json.JsonSyncDataMessage", "name":"org.asamk.signal.json.JsonSyncDataMessage",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -1819,9 +1791,6 @@
"name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged", "name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged",
"allPublicConstructors":true "allPublicConstructors":true
}, },
{
"name":"org.robolectric.Robolectric"
},
{ {
"name":"org.signal.cdsi.proto.ClientRequest", "name":"org.signal.cdsi.proto.ClientRequest",
"fields":[ "fields":[
@ -2288,7 +2257,6 @@
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[ "methods":[
{"name":"getCode","parameterTypes":[] },
{"name":"getNumber","parameterTypes":[] }, {"name":"getNumber","parameterTypes":[] },
{"name":"getRegistrationLock","parameterTypes":[] } {"name":"getRegistrationLock","parameterTypes":[] }
] ]
@ -2712,6 +2680,25 @@
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":true "allDeclaredConstructors":true
}, },
{
"name":"org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.Integer","java.lang.Integer","java.lang.Integer","boolean","java.util.List","boolean"] }]
},
{
"name":"org.whispersystems.signalservice.internal.push.RegistrationSessionRequestBody",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[
{"name":"getAccountAttributes","parameterTypes":[] },
{"name":"getRecoveryPassword","parameterTypes":[] },
{"name":"getSessionId","parameterTypes":[] },
{"name":"getSkipDeviceTransfer","parameterTypes":[] }
]
},
{ {
"name":"org.whispersystems.signalservice.internal.push.SendGroupMessageResponse", "name":"org.whispersystems.signalservice.internal.push.SendGroupMessageResponse",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2896,16 +2883,6 @@
{"name":"timestamp_"} {"name":"timestamp_"}
] ]
}, },
{
"name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$BodyRange",
"fields":[
{"name":"associatedValueCase_"},
{"name":"associatedValue_"},
{"name":"bitField0_"},
{"name":"length_"},
{"name":"start_"}
]
},
{ {
"name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact", "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact",
"fields":[ "fields":[
@ -3445,6 +3422,33 @@
"queryAllDeclaredConstructors":true, "queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
}, },
{
"name":"org.whispersystems.signalservice.internal.push.UpdateVerificationSessionRequestBody",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[
{"name":"getCaptcha","parameterTypes":[] },
{"name":"getMcc","parameterTypes":[] },
{"name":"getMnc","parameterTypes":[] },
{"name":"getPushChallenge","parameterTypes":[] },
{"name":"getPushToken","parameterTypes":[] },
{"name":"getPushTokenType","parameterTypes":[] }
]
},
{
"name":"org.whispersystems.signalservice.internal.push.VerificationSessionMetadataRequestBody",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[
{"name":"getMcc","parameterTypes":[] },
{"name":"getMnc","parameterTypes":[] },
{"name":"getNumber","parameterTypes":[] },
{"name":"getPushToken","parameterTypes":[] },
{"name":"getPushTokenType","parameterTypes":[] }
]
},
{ {
"name":"org.whispersystems.signalservice.internal.push.VerifyAccountResponse", "name":"org.whispersystems.signalservice.internal.push.VerifyAccountResponse",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -3544,6 +3548,7 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord", "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
"fields":[ "fields":[
{"name":"identifiers_"}, {"name":"identifiers_"},
{"name":"sourceDevice_"},
{"name":"version_"} {"name":"version_"}
] ]
}, },

View file

@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
@ -12,7 +13,7 @@ public interface RegistrationManager extends Closeable {
void register( void register(
boolean voiceVerification, String captcha boolean voiceVerification, String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException; ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException;
void verifyAccount( void verifyAccount(
String verificationCode, String pin String verificationCode, String pin

View file

@ -20,6 +20,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException; 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.api.UpdateProfile;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; 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.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.NumberVerificationUtils; import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; 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.ACI;
import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.IOException; import java.io.IOException;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
class RegistrationManagerImpl implements RegistrationManager { class RegistrationManagerImpl implements RegistrationManager {
private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class); private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class);
@ -106,7 +106,7 @@ class RegistrationManagerImpl implements RegistrationManager {
@Override @Override
public void register( public void register(
boolean voiceVerification, String captcha boolean voiceVerification, String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException { ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
if (account.isRegistered() if (account.isRegistered()
&& account.getServiceEnvironment() != null && account.getServiceEnvironment() != null
&& account.getServiceEnvironment() != serviceEnvironmentConfig.getType()) { && account.getServiceEnvironment() != serviceEnvironmentConfig.getType()) {
@ -117,14 +117,21 @@ class RegistrationManagerImpl implements RegistrationManager {
return; 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 @Override
public void verifyAccount( public void verifyAccount(
String verificationCode, String pin String verificationCode, String pin
) throws IOException, PinLockedException, IncorrectPinException { ) throws IOException, PinLockedException, IncorrectPinException {
final var result = NumberVerificationUtils.verifyNumber(verificationCode, var sessionId = account.getSessionId(account.getNumber());
final var result = NumberVerificationUtils.verifyNumber(sessionId,
verificationCode,
pin, pin,
pinHelper, pinHelper,
this::verifyAccountWithCode); this::verifyAccountWithCode);
@ -186,17 +193,7 @@ class RegistrationManagerImpl implements RegistrationManager {
userAgent, userAgent,
null, null,
ServiceConfig.AUTOMATIC_NETWORK_RETRY); ServiceConfig.AUTOMATIC_NETWORK_RETRY);
accountManager.setAccountAttributes(null, accountManager.setAccountAttributes(account.getAccountAttributes(null));
account.getLocalRegistrationId(),
true,
null,
account.getRegistrationLock(),
account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(),
capabilities,
account.isDiscoverableByPhoneNumber(),
account.getEncryptedDeviceName(),
account.getLocalPniRegistrationId());
account.setRegistered(true); account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary."); logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) { if (newManagerListener != null) {
@ -215,29 +212,18 @@ class RegistrationManagerImpl implements RegistrationManager {
return false; return false;
} }
private ServiceResponse<VerifyAccountResponse> verifyAccountWithCode( private VerifyAccountResponse verifyAccountWithCode(
final String verificationCode, final String registrationLock final String sessionId, final String verificationCode, final String registrationLock
) { ) throws IOException {
if (registrationLock == null) { try {
return accountManager.verifyAccount(verificationCode, Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId));
account.getLocalRegistrationId(), } catch (AlreadyVerifiedException e) {
true, // Already verified so can continue registering
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());
} }
return Utils.handleResponseException(accountManager.registerAccount(sessionId,
null,
account.getAccountAttributes(registrationLock),
true));
} }
@Override @Override

View file

@ -88,6 +88,10 @@ public class SignalDependencies {
return serviceEnvironmentConfig; return serviceEnvironmentConfig;
} }
public SignalSessionLock getSessionLock() {
return sessionLock;
}
public SignalServiceAccountManager getAccountManager() { public SignalServiceAccountManager getAccountManager() {
return getOrCreate(() -> accountManager, return getOrCreate(() -> accountManager,
() -> accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(), () -> accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
@ -115,7 +119,7 @@ public class SignalDependencies {
public GroupsV2Operations getGroupsV2Operations() { public GroupsV2Operations getGroupsV2Operations() {
return getOrCreate(() -> groupsV2Operations, return getOrCreate(() -> groupsV2Operations,
() -> groupsV2Operations = capabilities.isGv2() () -> groupsV2Operations = capabilities.getGv2()
? new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()), ? new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()),
ServiceConfig.GROUP_MAX_SIZE) ServiceConfig.GROUP_MAX_SIZE)
: null); : null);
@ -123,7 +127,7 @@ public class SignalDependencies {
private ClientZkOperations getClientZkOperations() { private ClientZkOperations getClientZkOperations() {
return getOrCreate(() -> clientZkOperations, return getOrCreate(() -> clientZkOperations,
() -> clientZkOperations = capabilities.isGv2() () -> clientZkOperations = capabilities.getGv2()
? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()) ? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration())
: null); : null);
} }

View file

@ -2,6 +2,13 @@ package org.asamk.signal.manager.api;
public class CaptchaRequiredException extends Exception { public class CaptchaRequiredException extends Exception {
private long nextAttemptTimestamp;
public CaptchaRequiredException(final long nextAttemptTimestamp) {
super("Captcha required");
this.nextAttemptTimestamp = nextAttemptTimestamp;
}
public CaptchaRequiredException(final String message) { public CaptchaRequiredException(final String message) {
super(message); super(message);
} }
@ -9,4 +16,8 @@ public class CaptchaRequiredException extends Exception {
public CaptchaRequiredException(final String message, final Throwable cause) { public CaptchaRequiredException(final String message, final Throwable cause) {
super(message, cause); super(message, cause);
} }
public long getNextAttemptTimestamp() {
return nextAttemptTimestamp;
}
} }

View file

@ -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;
}
}

View file

@ -7,10 +7,11 @@ import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException; 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.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils; 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.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; 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.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; 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.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
@ -128,9 +130,14 @@ public class AccountHelper {
public void startChangeNumber( public void startChangeNumber(
String newNumber, String captcha, boolean voiceVerification String newNumber, String captcha, boolean voiceVerification
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException { ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword()); 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( public void finishChangeNumber(
@ -140,17 +147,28 @@ public class AccountHelper {
final List<OutgoingPushMessage> deviceMessages = null; final List<OutgoingPushMessage> deviceMessages = null;
final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null; final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
final Map<String, Integer> pniRegistrationIds = 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, pin,
context.getPinHelper(), context.getPinHelper(),
(verificationCode1, registrationLock) -> dependencies.getAccountManager() (sessionId1, verificationCode1, registrationLock) -> {
.changeNumber(new ChangePhoneNumberRequest(newNumber, final var accountManager = dependencies.getAccountManager();
verificationCode1, try {
registrationLock, Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
account.getPniIdentityKeyPair().getPublicKey(), } catch (AlreadyVerifiedException e) {
deviceMessages, // Already verified so can continue changing number
devicePniSignedPreKeys, }
pniRegistrationIds))); return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
sessionId1,
null,
newNumber,
registrationLock,
account.getPniIdentityKeyPair().getPublicKey(),
deviceMessages,
devicePniSignedPreKeys,
pniRegistrationIds)));
});
// TODO handle response // TODO handle response
updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni())); updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
} }
@ -162,18 +180,7 @@ public class AccountHelper {
} }
public void updateAccountAttributes() throws IOException { public void updateAccountAttributes() throws IOException {
dependencies.getAccountManager() dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
.setAccountAttributes(null,
account.getLocalRegistrationId(),
true,
null,
account.getRegistrationLock(),
account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(),
ServiceConfig.capabilities,
account.isDiscoverableByPhoneNumber(),
account.getEncryptedDeviceName(),
account.getLocalPniRegistrationId());
} }
public void addDevice(DeviceLinkInfo deviceLinkInfo) throws IOException, InvalidDeviceLinkException { public void addDevice(DeviceLinkInfo deviceLinkInfo) throws IOException, InvalidDeviceLinkException {

View file

@ -80,7 +80,7 @@ public class IdentityHelper {
final var address = account.getRecipientAddressResolver() final var address = account.getRecipientAddressResolver()
.resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId)); .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId));
return Utils.computeSafetyNumber(capabilities.isUuid(), return Utils.computeSafetyNumber(capabilities.getUuid(),
account.getSelfRecipientAddress(), account.getSelfRecipientAddress(),
account.getAciIdentityKeyPair().getPublicKey(), account.getAciIdentityKeyPair().getPublicKey(),
address.getServiceId().equals(serviceId) address.getServiceId().equals(serviceId)

View file

@ -44,12 +44,14 @@ import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidMessageException; 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.message.DecryptionErrorMessage;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder;
import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; 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 {}", logger.debug("Received a sender key distribution message for distributionId {} from {}",
message.getDistributionId(), message.getDistributionId(),
protocolAddress); protocolAddress);
dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message); new SignalGroupSessionBuilder(dependencies.getSessionLock(),
new GroupSessionBuilder(account.getSenderKeyStore())).process(protocolAddress, message);
} }
if (content.getDecryptionErrorMessage().isPresent()) { if (content.getDecryptionErrorMessage().isPresent()) {

View file

@ -144,22 +144,28 @@ public class ReceiveHelper {
logger.debug("Checking for new message from server"); logger.debug("Checking for new message from server");
try { try {
isWaitingForMessage = true; 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; isWaitingForMessage = false;
final var recipientId = envelope1.hasSourceUuid() ? account.getRecipientResolver() for (final var it : batch) {
.resolveRecipient(envelope1.getSourceAddress()) : null; SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
logger.trace("Storing new message from {}", recipientId); it.getServerDeliveredTimestamp());
// store message on disk, before acknowledging receipt to the server final var recipientId = envelope1.hasSourceUuid() ? account.getRecipientResolver()
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); .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; isWaitingForMessage = false;
backOffCounter = 0; backOffCounter = 0;
if (result.isPresent()) { if (queueNotEmpty) {
if (remainingMessages > 0) { if (remainingMessages > 0) {
remainingMessages -= 1; remainingMessages -= 1;
} }
envelope = result.get(); envelope = cachedMessage[0].loadEnvelope();
logger.debug("New message received from server"); logger.debug("New message received from server");
} else { } else {
logger.debug("Received indicator that server queue is empty"); logger.debug("Received indicator that server queue is empty");

View file

@ -62,6 +62,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
import org.whispersystems.signalservice.api.SignalServiceDataStore; 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.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.ACI;
@ -97,6 +98,8 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
public class SignalAccount implements Closeable { public class SignalAccount implements Closeable {
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class); private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
@ -119,6 +122,8 @@ public class SignalAccount implements Closeable {
private String number; private String number;
private ACI aci; private ACI aci;
private PNI pni; private PNI pni;
private String sessionId;
private String sessionNumber;
private String encryptedDeviceName; private String encryptedDeviceName;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
private boolean isMultiDevice = false; 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); 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")) { if (rootNode.hasNonNull("deviceName")) {
encryptedDeviceName = rootNode.get("deviceName").asText(); encryptedDeviceName = rootNode.get("deviceName").asText();
} }
@ -926,6 +937,8 @@ public class SignalAccount implements Closeable {
.put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name()) .put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name())
.put("uuid", aci == null ? null : aci.toString()) .put("uuid", aci == null ? null : aci.toString())
.put("pni", pni == null ? null : pni.toString()) .put("pni", pni == null ? null : pni.toString())
.put("sessionId", sessionId)
.put("sessionNumber", sessionNumber)
.put("deviceName", encryptedDeviceName) .put("deviceName", encryptedDeviceName)
.put("deviceId", deviceId) .put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice) .put("isMultiDevice", isMultiDevice)
@ -1293,6 +1306,21 @@ public class SignalAccount implements Closeable {
save(); 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) { public ServiceId getAccountId(ServiceIdType serviceIdType) {
return serviceIdType.equals(ServiceIdType.ACI) ? aci : pni; return serviceIdType.equals(ServiceIdType.ACI) ? aci : pni;
} }
@ -1347,6 +1375,19 @@ public class SignalAccount implements Closeable {
return getRecipientResolver().resolveRecipient(getSelfRecipientAddress()); 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() { public byte[] getEncryptedDeviceName() {
return encryptedDeviceName == null ? null : Base64.getDecoder().decode(encryptedDeviceName); return encryptedDeviceName == null ? null : Base64.getDecoder().decode(encryptedDeviceName);
} }

View file

@ -15,21 +15,30 @@ public final class CachedMessage {
private final File file; private final File file;
private SignalServiceEnvelope envelope;
CachedMessage(final File file) { CachedMessage(final File file) {
this.file = file; this.file = file;
} }
CachedMessage(final File file, SignalServiceEnvelope envelope) {
this.file = file;
this.envelope = envelope;
}
File getFile() { File getFile() {
return file; return file;
} }
public SignalServiceEnvelope loadEnvelope() { public SignalServiceEnvelope loadEnvelope() {
try { if (envelope == null) {
return MessageCacheUtils.loadEnvelope(file); try {
} catch (Exception e) { envelope = MessageCacheUtils.loadEnvelope(file);
logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage(), e); } catch (Exception e) {
return null; logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage(), e);
}
} }
return envelope;
} }
public void delete() { public void delete() {

View file

@ -54,7 +54,7 @@ public class MessageCache {
try { try {
var cacheFile = getMessageCacheFile(recipientId, now, envelope.getTimestamp()); var cacheFile = getMessageCacheFile(recipientId, now, envelope.getTimestamp());
MessageCacheUtils.storeEnvelope(envelope, cacheFile); MessageCacheUtils.storeEnvelope(envelope, cacheFile);
return new CachedMessage(cacheFile); return new CachedMessage(cacheFile, envelope);
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage()); logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
return null; return null;

View file

@ -5,41 +5,94 @@ import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.PinHelper;
import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.kbs.MasterKey; 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.ServiceResponse;
import org.whispersystems.signalservice.internal.push.LockedException; 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 org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.function.Consumer;
public class NumberVerificationUtils { 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( public static void requestVerificationCode(
SignalServiceAccountManager accountManager, String captcha, boolean voiceVerification SignalServiceAccountManager accountManager, String sessionId, boolean voiceVerification
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException { ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); final ServiceResponse<RegistrationSessionMetadataResponse> response;
final ServiceResponse<RequestVerificationCodeResponse> response;
final var locale = Utils.getDefaultLocale(Locale.US); final var locale = Utils.getDefaultLocale(Locale.US);
if (voiceVerification) { if (voiceVerification) {
response = accountManager.requestVoiceVerificationCode(locale, response = accountManager.requestVoiceVerificationCode(sessionId, locale, false);
Optional.ofNullable(captcha),
Optional.empty(),
Optional.empty());
} else { } else {
response = accountManager.requestSmsVerificationCode(locale, response = accountManager.requestSmsVerificationCode(sessionId, locale, false);
false,
Optional.ofNullable(captcha),
Optional.empty(),
Optional.empty());
} }
try { try {
handleResponseException(response); Utils.handleResponseException(response);
} catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) { } catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
throw new CaptchaRequiredException(e.getMessage(), e); throw new CaptchaRequiredException(e.getMessage(), e);
} catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) { } catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) {
@ -51,11 +104,11 @@ public class NumberVerificationUtils {
} }
public static Pair<VerifyAccountResponse, MasterKey> verifyNumber( 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 { ) throws IOException, PinLockedException, IncorrectPinException {
verificationCode = verificationCode.replace("-", ""); verificationCode = verificationCode.replace("-", "");
try { try {
final var response = verifyAccountWithCode(verificationCode, null, verifier); final var response = verifier.verify(sessionId, verificationCode, null);
return new Pair<>(response, null); return new Pair<>(response, null);
} catch (LockedException e) { } catch (LockedException e) {
@ -72,7 +125,7 @@ public class NumberVerificationUtils {
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
VerifyAccountResponse response; VerifyAccountResponse response;
try { try {
response = verifyAccountWithCode(verificationCode, registrationLock, verifier); response = verifier.verify(sessionId, verificationCode, registrationLock);
} catch (LockedException _e) { } catch (LockedException _e) {
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
} }
@ -81,29 +134,53 @@ public class NumberVerificationUtils {
} }
} }
private static VerifyAccountResponse verifyAccountWithCode( private static RegistrationSessionMetadataResponse validateSession(
final String verificationCode, final String registrationLock, final Verifier verifier final SignalServiceAccountManager accountManager, final String sessionId
) throws IOException { ) throws IOException {
final var response = verifier.verify(verificationCode, registrationLock); if (sessionId == null || sessionId.isEmpty()) {
handleResponseException(response); throw new NoSuchSessionException();
return response.getResult().get(); }
return Utils.handleResponseException(accountManager.getRegistrationSession(sessionId));
} }
private static void handleResponseException(final ServiceResponse<?> response) throws IOException { private static RegistrationSessionMetadataResponse requestValidSession(
final var throwableOptional = response.getExecutionError().or(response::getApplicationError); final SignalServiceAccountManager accountManager
if (throwableOptional.isPresent()) { ) throws NoSuchSessionException, IOException {
if (throwableOptional.get() instanceof IOException) { return Utils.handleResponseException(accountManager.createRegistrationSession(null, "", ""));
throw (IOException) throwableOptional.get(); }
} else {
throw new IOException(throwableOptional.get()); 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 { public interface Verifier {
ServiceResponse<VerifyAccountResponse> verify( VerifyAccountResponse verify(
String verificationCode, String registrationLock String sessionId, String verificationCode, String registrationLock
); ) throws IOException;
} }
} }

View file

@ -8,6 +8,7 @@ import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.ServiceResponse;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
@ -128,4 +129,16 @@ public class Utils {
} }
return map; 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);
}
} }

View file

@ -16,7 +16,7 @@ dependencyResolutionManagement {
library("logback", "ch.qos.logback", "logback-classic").version("1.4.5") library("logback", "ch.qos.logback", "logback-classic").version("1.4.5")
library("signalservice", "com.github.turasa", "signal-service-java").version("2.15.3_unofficial_67") library("signalservice", "com.github.turasa", "signal-service-java").version("2.15.3_unofficial_68")
library("protobuf", "com.google.protobuf", "protobuf-javalite").version("3.22.0") library("protobuf", "com.google.protobuf", "protobuf-javalite").version("3.22.0")
library("sqlite", "org.xerial", "sqlite-jdbc").version("3.40.1.0") library("sqlite", "org.xerial", "sqlite-jdbc").version("3.40.1.0")
library("hikari", "com.zaxxer", "HikariCP").version("5.0.1") library("hikari", "com.zaxxer", "HikariCP").version("5.0.1")

View file

@ -13,7 +13,9 @@ import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.output.JsonWriter; import org.asamk.signal.output.JsonWriter;
import org.asamk.signal.util.DateUtils;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -65,6 +67,12 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration
) throws UserErrorException, IOErrorException { ) throws UserErrorException, IOErrorException {
try { try {
m.register(voiceVerification, captcha); m.register(voiceVerification, captcha);
} catch (RateLimitException e) {
String message = "Rate limit reached";
if (e.getNextAttemptTimestamp() > 0) {
message += "\nNext attempt may be tried at " + DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
}
throw new UserErrorException(message);
} catch (CaptchaRequiredException e) { } catch (CaptchaRequiredException e) {
String message; String message;
if (captcha == null) { if (captcha == null) {
@ -76,6 +84,10 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration
} else { } else {
message = "Invalid captcha given."; message = "Invalid captcha given.";
} }
if (e.getNextAttemptTimestamp() > 0) {
message += "\nNext Captcha may be provided at "
+ DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
}
throw new UserErrorException(message); throw new UserErrorException(message);
} catch (NonNormalizedPhoneNumberException e) { } catch (NonNormalizedPhoneNumberException e) {
throw new UserErrorException("Failed to register: " + e.getMessage(), e); throw new UserErrorException("Failed to register: " + e.getMessage(), e);

View file

@ -11,6 +11,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UserAlreadyExistsException; import org.asamk.signal.manager.api.UserAlreadyExistsException;
import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.DBusPath;
@ -59,6 +60,9 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
} }
try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) {
registrationManager.register(voiceVerification, captcha); registrationManager.register(voiceVerification, captcha);
} catch (RateLimitException e) {
String message = "Rate limit reached";
throw new SignalControl.Error.Failure(message);
} catch (CaptchaRequiredException e) { } catch (CaptchaRequiredException e) {
String message = captcha == null ? "Captcha required for verification." : "Invalid captcha given."; String message = captcha == null ? "Captcha required for verification." : "Invalid captcha given.";
throw new SignalControl.Error.RequiresCaptcha(message); throw new SignalControl.Error.RequiresCaptcha(message);