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":"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",
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]

View file

@ -78,22 +78,10 @@
"allDeclaredFields":true,
"allDeclaredMethods":true
},
{
"name":"com.google.protobuf.DescriptorMessageInfoFactory"
},
{
"name":"com.google.protobuf.ExtensionRegistry"
},
{
"name":"com.google.protobuf.ExtensionSchemaFull"
},
{
"name":"com.google.protobuf.GeneratedMessageLite",
"fields":[{"name":"unknownFields"}]
},
{
"name":"com.google.protobuf.GeneratedMessageV3"
},
{
"name":"com.google.protobuf.Internal$LongList",
"allDeclaredMethods":true
@ -108,19 +96,10 @@
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{
"name":"com.google.protobuf.MapFieldSchemaFull"
},
{
"name":"com.google.protobuf.NewInstanceSchemaFull"
},
{
"name":"com.google.protobuf.PrimitiveNonBoxingCollection",
"allDeclaredMethods":true
},
{
"name":"com.google.protobuf.UnknownFieldSetSchema"
},
{
"name":"com.sun.crypto.provider.AESCipher$General",
"methods":[{"name":"<init>","parameterTypes":[] }]
@ -465,9 +444,6 @@
{
"name":"javax.smartcardio.CardPermission"
},
{
"name":"libcore.io.Memory"
},
{
"name":"long",
"allDeclaredMethods":true,
@ -982,10 +958,6 @@
{"name":"startColor","parameterTypes":[] }
]
},
{
"name":"org.asamk.signal.json.JsonStreamSerializer",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.asamk.signal.json.JsonSyncDataMessage",
"allDeclaredFields":true,
@ -1819,9 +1791,6 @@
"name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged",
"allPublicConstructors":true
},
{
"name":"org.robolectric.Robolectric"
},
{
"name":"org.signal.cdsi.proto.ClientRequest",
"fields":[
@ -2288,7 +2257,6 @@
"queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[
{"name":"getCode","parameterTypes":[] },
{"name":"getNumber","parameterTypes":[] },
{"name":"getRegistrationLock","parameterTypes":[] }
]
@ -2712,6 +2680,25 @@
"allDeclaredMethods":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",
"allDeclaredFields":true,
@ -2896,16 +2883,6 @@
{"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",
"fields":[
@ -3445,6 +3422,33 @@
"queryAllDeclaredConstructors":true,
"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",
"allDeclaredFields":true,
@ -3544,6 +3548,7 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
"fields":[
{"name":"identifiers_"},
{"name":"sourceDevice_"},
{"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.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import java.io.Closeable;
import java.io.IOException;
@ -12,7 +13,7 @@ public interface RegistrationManager extends Closeable {
void register(
boolean voiceVerification, String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException;
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException;
void verifyAccount(
String verificationCode, String pin

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -62,6 +62,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ACI;
@ -97,6 +98,8 @@ import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
public class SignalAccount implements Closeable {
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
@ -119,6 +122,8 @@ public class SignalAccount implements Closeable {
private String number;
private ACI aci;
private PNI pni;
private String sessionId;
private String sessionNumber;
private String encryptedDeviceName;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
private boolean isMultiDevice = false;
@ -551,6 +556,12 @@ public class SignalAccount implements Closeable {
throw new IOException("Config file contains an invalid pni, needs to be a valid UUID", e);
}
}
if (rootNode.hasNonNull("sessionId")) {
sessionId = rootNode.get("sessionId").asText();
}
if (rootNode.hasNonNull("sessionNumber")) {
sessionNumber = rootNode.get("sessionNumber").asText();
}
if (rootNode.hasNonNull("deviceName")) {
encryptedDeviceName = rootNode.get("deviceName").asText();
}
@ -926,6 +937,8 @@ public class SignalAccount implements Closeable {
.put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name())
.put("uuid", aci == null ? null : aci.toString())
.put("pni", pni == null ? null : pni.toString())
.put("sessionId", sessionId)
.put("sessionNumber", sessionNumber)
.put("deviceName", encryptedDeviceName)
.put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice)
@ -1293,6 +1306,21 @@ public class SignalAccount implements Closeable {
save();
}
public AccountAttributes getAccountAttributes(String registrationLock) {
return new AccountAttributes(null,
getLocalRegistrationId(),
true,
null,
registrationLock != null ? registrationLock : getRegistrationLock(),
getSelfUnidentifiedAccessKey(),
isUnrestrictedUnidentifiedAccess(),
capabilities,
isDiscoverableByPhoneNumber(),
encryptedDeviceName,
getLocalPniRegistrationId(),
null); // TODO recoveryPassword?
}
public ServiceId getAccountId(ServiceIdType serviceIdType) {
return serviceIdType.equals(ServiceIdType.ACI) ? aci : pni;
}
@ -1347,6 +1375,19 @@ public class SignalAccount implements Closeable {
return getRecipientResolver().resolveRecipient(getSelfRecipientAddress());
}
public String getSessionId(final String forNumber) {
if (!forNumber.equals(sessionNumber)) {
return null;
}
return sessionId;
}
public void setSessionId(final String sessionNumber, final String sessionId) {
this.sessionNumber = sessionNumber;
this.sessionId = sessionId;
save();
}
public byte[] getEncryptedDeviceName() {
return encryptedDeviceName == null ? null : Base64.getDecoder().decode(encryptedDeviceName);
}

View file

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

View file

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

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.Pair;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.helper.PinHelper;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Consumer;
public class NumberVerificationUtils {
public static String handleVerificationSession(
SignalServiceAccountManager accountManager,
String sessionId,
Consumer<String> sessionIdSaver,
boolean voiceVerification,
String captcha
) throws CaptchaRequiredException, IOException, RateLimitException {
RegistrationSessionMetadataResponse sessionResponse;
try {
sessionResponse = getValidSession(accountManager, sessionId);
} catch (PushChallengeRequiredException |
org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
if (captcha != null) {
sessionResponse = submitCaptcha(accountManager, sessionId, captcha);
} else {
throw new CaptchaRequiredException("Captcha Required");
}
}
sessionId = sessionResponse.getBody().getId();
sessionIdSaver.accept(sessionId);
if (sessionResponse.getBody().getVerified()) {
return sessionId;
}
if (sessionResponse.getBody().getAllowedToRequestCode()) {
return sessionId;
}
final var nextAttempt = voiceVerification
? sessionResponse.getBody().getNextCall()
: sessionResponse.getBody().getNextSms();
if (nextAttempt != null && nextAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextAttempt * 1000;
throw new RateLimitException(timestamp);
}
final var nextVerificationAttempt = sessionResponse.getBody().getNextVerificationAttempt();
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextVerificationAttempt * 1000;
throw new CaptchaRequiredException(timestamp);
}
if (sessionResponse.getBody().getRequestedInformation().contains("captcha")) {
if (captcha != null) {
sessionResponse = submitCaptcha(accountManager, sessionId, captcha);
}
if (!sessionResponse.getBody().getAllowedToRequestCode()) {
throw new CaptchaRequiredException("Captcha Required");
}
}
return sessionId;
}
public static void requestVerificationCode(
SignalServiceAccountManager accountManager, String captcha, boolean voiceVerification
SignalServiceAccountManager accountManager, String sessionId, boolean voiceVerification
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
final ServiceResponse<RequestVerificationCodeResponse> response;
final ServiceResponse<RegistrationSessionMetadataResponse> response;
final var locale = Utils.getDefaultLocale(Locale.US);
if (voiceVerification) {
response = accountManager.requestVoiceVerificationCode(locale,
Optional.ofNullable(captcha),
Optional.empty(),
Optional.empty());
response = accountManager.requestVoiceVerificationCode(sessionId, locale, false);
} else {
response = accountManager.requestSmsVerificationCode(locale,
false,
Optional.ofNullable(captcha),
Optional.empty(),
Optional.empty());
response = accountManager.requestSmsVerificationCode(sessionId, locale, false);
}
try {
handleResponseException(response);
Utils.handleResponseException(response);
} catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
throw new CaptchaRequiredException(e.getMessage(), e);
} catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) {
@ -51,11 +104,11 @@ public class NumberVerificationUtils {
}
public static Pair<VerifyAccountResponse, MasterKey> verifyNumber(
String verificationCode, String pin, PinHelper pinHelper, Verifier verifier
String sessionId, String verificationCode, String pin, PinHelper pinHelper, Verifier verifier
) throws IOException, PinLockedException, IncorrectPinException {
verificationCode = verificationCode.replace("-", "");
try {
final var response = verifyAccountWithCode(verificationCode, null, verifier);
final var response = verifier.verify(sessionId, verificationCode, null);
return new Pair<>(response, null);
} catch (LockedException e) {
@ -72,7 +125,7 @@ public class NumberVerificationUtils {
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
VerifyAccountResponse response;
try {
response = verifyAccountWithCode(verificationCode, registrationLock, verifier);
response = verifier.verify(sessionId, verificationCode, registrationLock);
} catch (LockedException _e) {
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
}
@ -81,29 +134,53 @@ public class NumberVerificationUtils {
}
}
private static VerifyAccountResponse verifyAccountWithCode(
final String verificationCode, final String registrationLock, final Verifier verifier
private static RegistrationSessionMetadataResponse validateSession(
final SignalServiceAccountManager accountManager, final String sessionId
) throws IOException {
final var response = verifier.verify(verificationCode, registrationLock);
handleResponseException(response);
return response.getResult().get();
if (sessionId == null || sessionId.isEmpty()) {
throw new NoSuchSessionException();
}
return Utils.handleResponseException(accountManager.getRegistrationSession(sessionId));
}
private static void handleResponseException(final ServiceResponse<?> response) throws IOException {
final var throwableOptional = response.getExecutionError().or(response::getApplicationError);
if (throwableOptional.isPresent()) {
if (throwableOptional.get() instanceof IOException) {
throw (IOException) throwableOptional.get();
} else {
throw new IOException(throwableOptional.get());
private static RegistrationSessionMetadataResponse requestValidSession(
final SignalServiceAccountManager accountManager
) throws NoSuchSessionException, IOException {
return Utils.handleResponseException(accountManager.createRegistrationSession(null, "", ""));
}
private static RegistrationSessionMetadataResponse getValidSession(
final SignalServiceAccountManager accountManager, final String sessionId
) throws IOException {
try {
return validateSession(accountManager, sessionId);
} catch (NoSuchSessionException e) {
return requestValidSession(accountManager);
}
}
private static RegistrationSessionMetadataResponse submitCaptcha(
SignalServiceAccountManager accountManager, String sessionId, String captcha
) throws IOException, CaptchaRequiredException {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
try {
return Utils.handleResponseException(accountManager.submitCaptchaToken(sessionId, captcha));
} catch (PushChallengeRequiredException |
org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException |
TokenNotAcceptedException _e) {
throw new CaptchaRequiredException("Captcha not accepted");
} catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 400) {
throw new CaptchaRequiredException("Captcha has invalid format");
}
throw e;
}
}
public interface Verifier {
ServiceResponse<VerifyAccountResponse> verify(
String verificationCode, String registrationLock
);
VerifyAccountResponse verify(
String sessionId, String verificationCode, String registrationLock
) throws IOException;
}
}

View file

@ -8,6 +8,7 @@ import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.ServiceResponse;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -128,4 +129,16 @@ public class Utils {
}
return map;
}
public static <T> T handleResponseException(final ServiceResponse<T> response) throws IOException {
final var throwableOptional = response.getExecutionError().or(response::getApplicationError);
if (throwableOptional.isPresent()) {
if (throwableOptional.get() instanceof IOException) {
throw (IOException) throwableOptional.get();
} else {
throw new IOException(throwableOptional.get());
}
}
return response.getResult().orElse(null);
}
}

View file

@ -16,7 +16,7 @@ dependencyResolutionManagement {
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("sqlite", "org.xerial", "sqlite-jdbc").version("3.40.1.0")
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.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.output.JsonWriter;
import org.asamk.signal.util.DateUtils;
import java.io.IOException;
import java.util.List;
@ -65,6 +67,12 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration
) throws UserErrorException, IOErrorException {
try {
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) {
String message;
if (captcha == null) {
@ -76,6 +84,10 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration
} else {
message = "Invalid captcha given.";
}
if (e.getNextAttemptTimestamp() > 0) {
message += "\nNext Captcha may be provided at "
+ DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
}
throw new UserErrorException(message);
} catch (NonNormalizedPhoneNumberException 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.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UserAlreadyExistsException;
import org.freedesktop.dbus.DBusPath;
@ -59,6 +60,9 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
}
try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) {
registrationManager.register(voiceVerification, captcha);
} catch (RateLimitException e) {
String message = "Rate limit reached";
throw new SignalControl.Error.Failure(message);
} catch (CaptchaRequiredException e) {
String message = captcha == null ? "Captcha required for verification." : "Invalid captcha given.";
throw new SignalControl.Error.RequiresCaptcha(message);