mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20: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
|
@ -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"] }]
|
||||||
|
|
|
@ -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_"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue