mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20:39 +00:00
parent
56ee173d03
commit
33c4e17c0d
15 changed files with 382 additions and 63 deletions
|
@ -380,6 +380,9 @@
|
||||||
"name":"java.util.Locale",
|
"name":"java.util.Locale",
|
||||||
"methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]
|
"methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"java.util.Map"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"java.util.Optional",
|
"name":"java.util.Optional",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
@ -505,9 +508,15 @@
|
||||||
{
|
{
|
||||||
"name":"kotlin.collections.List"
|
"name":"kotlin.collections.List"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"kotlin.collections.Map"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"kotlin.collections.MutableList"
|
"name":"kotlin.collections.MutableList"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"kotlin.collections.MutableMap"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"kotlin.jvm.JvmStatic",
|
"name":"kotlin.jvm.JvmStatic",
|
||||||
"queryAllDeclaredMethods":true
|
"queryAllDeclaredMethods":true
|
||||||
|
@ -545,7 +554,7 @@
|
||||||
"name":"org.asamk.Signal",
|
"name":"org.asamk.Signal",
|
||||||
"allDeclaredMethods":true,
|
"allDeclaredMethods":true,
|
||||||
"allDeclaredClasses":true,
|
"allDeclaredClasses":true,
|
||||||
"methods":[{"name":"getSelfNumber","parameterTypes":[] }, {"name":"sendGroupMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","byte[]"] }, {"name":"sendMessage","parameterTypes":["java.lang.String","java.util.List","java.lang.String"] }, {"name":"sendMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","java.util.List"] }, {"name":"subscribeReceive","parameterTypes":[] }, {"name":"unsubscribeReceive","parameterTypes":[] }]
|
"methods":[{"name":"getContactName","parameterTypes":["java.lang.String"] }, {"name":"getSelfNumber","parameterTypes":[] }, {"name":"sendGroupMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","byte[]"] }, {"name":"sendMessage","parameterTypes":["java.lang.String","java.util.List","java.lang.String"] }, {"name":"sendMessageReaction","parameterTypes":["java.lang.String","boolean","java.lang.String","long","java.util.List"] }, {"name":"subscribeReceive","parameterTypes":[] }, {"name":"unsubscribeReceive","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"org.asamk.Signal$Configuration",
|
"name":"org.asamk.Signal$Configuration",
|
||||||
|
@ -2049,9 +2058,10 @@
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest",
|
"name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
"allDeclaredClasses":true,
|
||||||
"queryAllDeclaredMethods":true,
|
"queryAllDeclaredMethods":true,
|
||||||
"queryAllDeclaredConstructors":true,
|
"queryAllDeclaredConstructors":true,
|
||||||
"methods":[{"name":"getNumber","parameterTypes":[] }, {"name":"getRegistrationLock","parameterTypes":[] }]
|
"methods":[{"name":"getDeviceMessages","parameterTypes":[] }, {"name":"getDevicePniSignedPrekeys","parameterTypes":[] }, {"name":"getNumber","parameterTypes":[] }, {"name":"getPniIdentityKey","parameterTypes":[] }, {"name":"getPniRegistrationIds","parameterTypes":[] }, {"name":"getRegistrationLock","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.signalservice.api.groupsv2.CredentialResponse",
|
"name":"org.whispersystems.signalservice.api.groupsv2.CredentialResponse",
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.Configuration;
|
import org.asamk.signal.manager.api.Configuration;
|
||||||
import org.asamk.signal.manager.api.Device;
|
import org.asamk.signal.manager.api.Device;
|
||||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||||
|
@ -13,16 +14,20 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
||||||
import org.asamk.signal.manager.api.Identity;
|
import org.asamk.signal.manager.api.Identity;
|
||||||
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||||
|
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.InvalidStickerException;
|
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
import org.asamk.signal.manager.api.LastGroupAdminException;
|
||||||
import org.asamk.signal.manager.api.Message;
|
import org.asamk.signal.manager.api.Message;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
import org.asamk.signal.manager.api.Pair;
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
||||||
|
import org.asamk.signal.manager.api.PinLockedException;
|
||||||
|
import org.asamk.signal.manager.api.RateLimitException;
|
||||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||||
import org.asamk.signal.manager.api.Recipient;
|
import org.asamk.signal.manager.api.Recipient;
|
||||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||||
|
@ -107,6 +112,14 @@ public interface Manager extends Closeable {
|
||||||
*/
|
*/
|
||||||
void deleteUsername() throws IOException;
|
void deleteUsername() throws IOException;
|
||||||
|
|
||||||
|
void startChangeNumber(
|
||||||
|
String newNumber, boolean voiceVerification, String captcha
|
||||||
|
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
|
void finishChangeNumber(
|
||||||
|
String newNumber, String verificationCode, String pin
|
||||||
|
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
void unregister() throws IOException;
|
void unregister() throws IOException;
|
||||||
|
|
||||||
void deleteAccount() throws IOException;
|
void deleteAccount() throws IOException;
|
||||||
|
|
|
@ -14,16 +14,20 @@ import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
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.SignalProtocolAddress;
|
||||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||||
|
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.signal.libsignal.usernames.Username;
|
import org.signal.libsignal.usernames.Username;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
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.AlreadyVerifiedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
|
@ -31,16 +35,21 @@ import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionExc
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
||||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SyncMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||||
import org.whispersystems.util.Base64UrlSafe;
|
import org.whispersystems.util.Base64UrlSafe;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
|
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
||||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||||
|
|
||||||
public class AccountHelper {
|
public class AccountHelper {
|
||||||
|
@ -139,7 +148,7 @@ public class AccountHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startChangeNumber(
|
public void startChangeNumber(
|
||||||
String newNumber, String captcha, boolean voiceVerification
|
String newNumber, boolean voiceVerification, String captcha
|
||||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
|
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
|
||||||
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
|
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
|
||||||
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
|
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
|
||||||
|
@ -153,12 +162,92 @@ public class AccountHelper {
|
||||||
public void finishChangeNumber(
|
public void finishChangeNumber(
|
||||||
String newNumber, String verificationCode, String pin
|
String newNumber, String verificationCode, String pin
|
||||||
) throws IncorrectPinException, PinLockedException, IOException {
|
) throws IncorrectPinException, PinLockedException, IOException {
|
||||||
// TODO create new PNI identity key
|
for (var attempts = 0; attempts < 5; attempts++) {
|
||||||
final List<OutgoingPushMessage> deviceMessages = null;
|
try {
|
||||||
final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
|
finishChangeNumberInternal(newNumber, verificationCode, pin);
|
||||||
final Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys = null;
|
break;
|
||||||
final Map<String, Integer> pniRegistrationIds = null;
|
} catch (MismatchedDevicesException e) {
|
||||||
var sessionId = account.getSessionId(account.getNumber());
|
logger.debug("Change number failed with mismatched devices, retrying.");
|
||||||
|
try {
|
||||||
|
dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
|
||||||
|
} catch (UntrustedIdentityException ex) {
|
||||||
|
throw new AssertionError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishChangeNumberInternal(
|
||||||
|
String newNumber, String verificationCode, String pin
|
||||||
|
) throws IncorrectPinException, PinLockedException, IOException {
|
||||||
|
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
|
||||||
|
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
|
||||||
|
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
|
||||||
|
final var devicePniLastResortKyberPreKeys = new HashMap<Integer, KyberPreKeyEntity>();
|
||||||
|
final var pniRegistrationIds = new HashMap<Integer, Integer>();
|
||||||
|
|
||||||
|
final var selfDeviceId = account.getDeviceId();
|
||||||
|
SyncMessage.PniChangeNumber selfChangeNumber = null;
|
||||||
|
|
||||||
|
final var deviceIds = new ArrayList<Integer>();
|
||||||
|
deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||||
|
final var aci = account.getAci();
|
||||||
|
final var accountDataStore = account.getSignalServiceDataStore().aci();
|
||||||
|
final var subDeviceSessions = accountDataStore.getSubDeviceSessions(aci.toString())
|
||||||
|
.stream()
|
||||||
|
.filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
|
||||||
|
deviceId)))
|
||||||
|
.toList();
|
||||||
|
deviceIds.addAll(subDeviceSessions);
|
||||||
|
|
||||||
|
final var messageSender = dependencies.getMessageSender();
|
||||||
|
for (final var deviceId : deviceIds) {
|
||||||
|
// Signed Prekey
|
||||||
|
final var signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
|
||||||
|
pniIdentity.getPrivateKey());
|
||||||
|
final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
|
||||||
|
signedPreKeyRecord.getKeyPair().getPublicKey(),
|
||||||
|
signedPreKeyRecord.getSignature());
|
||||||
|
devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
|
||||||
|
|
||||||
|
// Last-resort kyber prekey
|
||||||
|
final var lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(
|
||||||
|
PREKEY_MAXIMUM_ID), pniIdentity.getPrivateKey());
|
||||||
|
final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
|
||||||
|
lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
|
||||||
|
lastResortKyberPreKeyRecord.getSignature());
|
||||||
|
devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
|
||||||
|
|
||||||
|
// Registration Id
|
||||||
|
var pniRegistrationId = -1;
|
||||||
|
while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
|
||||||
|
pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
||||||
|
}
|
||||||
|
pniRegistrationIds.put(deviceId, pniRegistrationId);
|
||||||
|
|
||||||
|
// Device Message
|
||||||
|
final var pniChangeNumber = new SyncMessage.PniChangeNumber.Builder().identityKeyPair(ByteString.of(
|
||||||
|
pniIdentity.serialize()))
|
||||||
|
.signedPreKey(ByteString.of(signedPreKeyRecord.serialize()))
|
||||||
|
.lastResortKyberPreKey(ByteString.of(lastResortKyberPreKeyRecord.serialize()))
|
||||||
|
.registrationId(pniRegistrationId)
|
||||||
|
.newE164(newNumber)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if (deviceId == selfDeviceId) {
|
||||||
|
selfChangeNumber = pniChangeNumber;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
|
||||||
|
pniChangeNumber);
|
||||||
|
encryptedDeviceMessages.add(message);
|
||||||
|
} catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var sessionId = account.getSessionId(newNumber);
|
||||||
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
pin,
|
pin,
|
||||||
|
@ -166,7 +255,7 @@ public class AccountHelper {
|
||||||
(sessionId1, verificationCode1, registrationLock) -> {
|
(sessionId1, verificationCode1, registrationLock) -> {
|
||||||
final var accountManager = dependencies.getAccountManager();
|
final var accountManager = dependencies.getAccountManager();
|
||||||
try {
|
try {
|
||||||
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
|
Utils.handleResponseException(accountManager.verifyAccount(verificationCode1, sessionId1));
|
||||||
} catch (AlreadyVerifiedException e) {
|
} catch (AlreadyVerifiedException e) {
|
||||||
// Already verified so can continue changing number
|
// Already verified so can continue changing number
|
||||||
}
|
}
|
||||||
|
@ -175,14 +264,42 @@ public class AccountHelper {
|
||||||
null,
|
null,
|
||||||
newNumber,
|
newNumber,
|
||||||
registrationLock,
|
registrationLock,
|
||||||
account.getPniIdentityKeyPair().getPublicKey(),
|
pniIdentity.getPublicKey(),
|
||||||
deviceMessages,
|
encryptedDeviceMessages,
|
||||||
devicePniSignedPreKeys,
|
Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
|
||||||
devicePniLastResortKyberPrekeys,
|
Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
|
||||||
pniRegistrationIds)));
|
Utils.mapKeys(pniRegistrationIds, Object::toString))));
|
||||||
});
|
});
|
||||||
// TODO handle response
|
|
||||||
updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
|
final var updatePni = PNI.parseOrThrow(result.first().getPni());
|
||||||
|
if (updatePni.equals(account.getPni())) {
|
||||||
|
logger.debug("PNI is unchanged after change number");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handlePniChangeNumberMessage(
|
||||||
|
final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
|
||||||
|
) {
|
||||||
|
if (pniChangeNumber.identityKeyPair != null
|
||||||
|
&& pniChangeNumber.registrationId != null
|
||||||
|
&& pniChangeNumber.signedPreKey != null) {
|
||||||
|
logger.debug("New PNI: {}", updatedPni);
|
||||||
|
try {
|
||||||
|
setPni(updatedPni,
|
||||||
|
new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
|
||||||
|
pniChangeNumber.newE164,
|
||||||
|
pniChangeNumber.registrationId,
|
||||||
|
new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
|
||||||
|
pniChangeNumber.lastResortKyberPreKey != null
|
||||||
|
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
|
||||||
|
: null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to handle change number message", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int USERNAME_MIN_LENGTH = 3;
|
public static final int USERNAME_MIN_LENGTH = 3;
|
||||||
|
|
|
@ -40,12 +40,9 @@ import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||||
import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
||||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
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.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
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.KyberPreKeyRecord;
|
|
||||||
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;
|
||||||
|
@ -67,7 +64,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
import org.whispersystems.signalservice.internal.push.Envelope;
|
||||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||||
|
@ -618,24 +614,10 @@ public final class IncomingMessageHandler {
|
||||||
if (syncMessage.getPniChangeNumber().isPresent()) {
|
if (syncMessage.getPniChangeNumber().isPresent()) {
|
||||||
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
|
||||||
logger.debug("Received PNI change number sync message, applying.");
|
logger.debug("Received PNI change number sync message, applying.");
|
||||||
if (pniChangeNumber.identityKeyPair != null
|
final var updatedPniString = envelope.getUpdatedPni();
|
||||||
&& pniChangeNumber.registrationId != null
|
if (updatedPniString != null && !updatedPniString.isEmpty()) {
|
||||||
&& pniChangeNumber.signedPreKey != null
|
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
|
||||||
&& !envelope.getUpdatedPni().isEmpty()) {
|
context.getAccountHelper().handlePniChangeNumberMessage(pniChangeNumber, updatedPni);
|
||||||
logger.debug("New PNI: {}", envelope.getUpdatedPni());
|
|
||||||
try {
|
|
||||||
final var updatedPni = PNI.parseOrThrow(envelope.getUpdatedPni());
|
|
||||||
context.getAccountHelper()
|
|
||||||
.setPni(updatedPni,
|
|
||||||
new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
|
|
||||||
pniChangeNumber.newE164,
|
|
||||||
pniChangeNumber.registrationId,
|
|
||||||
new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
|
|
||||||
pniChangeNumber.lastResortKyberPreKey != null ? new KyberPreKeyRecord(
|
|
||||||
pniChangeNumber.lastResortKyberPreKey.toByteArray()) : null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Failed to handle change number message", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actions;
|
return actions;
|
||||||
|
|
|
@ -112,7 +112,12 @@ public class PreKeyHelper {
|
||||||
preKeyRecords,
|
preKeyRecords,
|
||||||
lastResortKyberPreKeyRecord,
|
lastResortKyberPreKeyRecord,
|
||||||
kyberPreKeyRecords);
|
kyberPreKeyRecords);
|
||||||
dependencies.getAccountManager().setPreKeys(preKeyUpload);
|
try {
|
||||||
|
dependencies.getAccountManager().setPreKeys(preKeyUpload);
|
||||||
|
} catch (AuthorizationFailedException e) {
|
||||||
|
// This can happen when the primary device has changed phone number
|
||||||
|
logger.warn("Failed to updated pre keys: {}", e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanSignedPreKeys((serviceIdType));
|
cleanSignedPreKeys((serviceIdType));
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.asamk.signal.manager.internal;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.Configuration;
|
import org.asamk.signal.manager.api.Configuration;
|
||||||
import org.asamk.signal.manager.api.Device;
|
import org.asamk.signal.manager.api.Device;
|
||||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||||
|
@ -30,17 +31,21 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
||||||
import org.asamk.signal.manager.api.Identity;
|
import org.asamk.signal.manager.api.Identity;
|
||||||
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||||
|
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.InvalidStickerException;
|
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
import org.asamk.signal.manager.api.LastGroupAdminException;
|
||||||
import org.asamk.signal.manager.api.Message;
|
import org.asamk.signal.manager.api.Message;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
import org.asamk.signal.manager.api.Pair;
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
||||||
|
import org.asamk.signal.manager.api.PinLockedException;
|
||||||
import org.asamk.signal.manager.api.Profile;
|
import org.asamk.signal.manager.api.Profile;
|
||||||
|
import org.asamk.signal.manager.api.RateLimitException;
|
||||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||||
import org.asamk.signal.manager.api.Recipient;
|
import org.asamk.signal.manager.api.Recipient;
|
||||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||||
|
@ -317,6 +322,26 @@ public class ManagerImpl implements Manager {
|
||||||
context.getAccountHelper().deleteUsername();
|
context.getAccountHelper().deleteUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startChangeNumber(
|
||||||
|
String newNumber, boolean voiceVerification, String captcha
|
||||||
|
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException {
|
||||||
|
if (!account.isPrimaryDevice()) {
|
||||||
|
throw new NotPrimaryDeviceException();
|
||||||
|
}
|
||||||
|
context.getAccountHelper().startChangeNumber(newNumber, voiceVerification, captcha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finishChangeNumber(
|
||||||
|
String newNumber, String verificationCode, String pin
|
||||||
|
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
|
||||||
|
if (!account.isPrimaryDevice()) {
|
||||||
|
throw new NotPrimaryDeviceException();
|
||||||
|
}
|
||||||
|
context.getAccountHelper().finishChangeNumber(newNumber, verificationCode, pin);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregister() throws IOException {
|
public void unregister() throws IOException {
|
||||||
context.getAccountHelper().unregister();
|
context.getAccountHelper().unregister();
|
||||||
|
|
|
@ -1383,7 +1383,6 @@ public class SignalAccount implements Closeable {
|
||||||
if (oldPni != null && !oldPni.equals(updatedPni)) {
|
if (oldPni != null && !oldPni.equals(updatedPni)) {
|
||||||
// Clear data for old PNI
|
// Clear data for old PNI
|
||||||
identityKeyStore.deleteIdentity(oldPni);
|
identityKeyStore.deleteIdentity(oldPni);
|
||||||
clearAllPreKeys(ServiceIdType.PNI);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pniAccountData.setServiceId(updatedPni);
|
this.pniAccountData.setServiceId(updatedPni);
|
||||||
|
@ -1400,11 +1399,14 @@ public class SignalAccount implements Closeable {
|
||||||
setPniIdentityKeyPair(pniIdentityKeyPair);
|
setPniIdentityKeyPair(pniIdentityKeyPair);
|
||||||
pniAccountData.setLocalRegistrationId(localPniRegistrationId);
|
pniAccountData.setLocalRegistrationId(localPniRegistrationId);
|
||||||
|
|
||||||
final var preKeyMetadata = getAccountData(ServiceIdType.PNI).getPreKeyMetadata();
|
final AccountData<? extends ServiceId> accountData = getAccountData(ServiceIdType.PNI);
|
||||||
|
final var preKeyMetadata = accountData.getPreKeyMetadata();
|
||||||
preKeyMetadata.nextSignedPreKeyId = pniSignedPreKey.getId();
|
preKeyMetadata.nextSignedPreKeyId = pniSignedPreKey.getId();
|
||||||
|
accountData.getSignedPreKeyStore().removeSignedPreKey(pniSignedPreKey.getId());
|
||||||
addSignedPreKey(ServiceIdType.PNI, pniSignedPreKey);
|
addSignedPreKey(ServiceIdType.PNI, pniSignedPreKey);
|
||||||
if (lastResortKyberPreKey != null) {
|
if (lastResortKyberPreKey != null) {
|
||||||
preKeyMetadata.nextKyberPreKeyId = lastResortKyberPreKey.getId();
|
preKeyMetadata.nextKyberPreKeyId = lastResortKyberPreKey.getId();
|
||||||
|
accountData.getKyberPreKeyStore().removeKyberPreKey(lastResortKyberPreKey.getId());
|
||||||
addLastResortKyberPreKey(ServiceIdType.PNI, lastResortKyberPreKey);
|
addLastResortKyberPreKey(ServiceIdType.PNI, lastResortKyberPreKey);
|
||||||
}
|
}
|
||||||
save();
|
save();
|
||||||
|
|
|
@ -7,6 +7,8 @@ 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.api.RateLimitException;
|
||||||
import org.asamk.signal.manager.helper.PinHelper;
|
import org.asamk.signal.manager.helper.PinHelper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
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.NoSuchSessionException;
|
||||||
|
@ -24,6 +26,8 @@ import java.util.function.Consumer;
|
||||||
|
|
||||||
public class NumberVerificationUtils {
|
public class NumberVerificationUtils {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(NumberVerificationUtils.class);
|
||||||
|
|
||||||
public static String handleVerificationSession(
|
public static String handleVerificationSession(
|
||||||
SignalServiceAccountManager accountManager,
|
SignalServiceAccountManager accountManager,
|
||||||
String sessionId,
|
String sessionId,
|
||||||
|
@ -143,7 +147,7 @@ public class NumberVerificationUtils {
|
||||||
|
|
||||||
private static RegistrationSessionMetadataResponse requestValidSession(
|
private static RegistrationSessionMetadataResponse requestValidSession(
|
||||||
final SignalServiceAccountManager accountManager
|
final SignalServiceAccountManager accountManager
|
||||||
) throws NoSuchSessionException, IOException {
|
) throws IOException {
|
||||||
return Utils.handleResponseException(accountManager.createRegistrationSession(null, "", ""));
|
return Utils.handleResponseException(accountManager.createRegistrationSession(null, "", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +157,7 @@ public class NumberVerificationUtils {
|
||||||
try {
|
try {
|
||||||
return validateSession(accountManager, sessionId);
|
return validateSession(accountManager, sessionId);
|
||||||
} catch (NoSuchSessionException e) {
|
} catch (NoSuchSessionException e) {
|
||||||
|
logger.debug("No registration session, creating new one.");
|
||||||
return requestValidSession(accountManager);
|
return requestValidSession(accountManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ import java.util.Spliterator;
|
||||||
import java.util.Spliterators;
|
import java.util.Spliterators;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
@ -122,6 +124,10 @@ public class Utils {
|
||||||
}, leftStream.isParallel() || rightStream.isParallel());
|
}, leftStream.isParallel() || rightStream.isParallel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <OK, NK, V> Map<NK, V> mapKeys(Map<OK, V> map, Function<OK, NK> keyMapper) {
|
||||||
|
return map.entrySet().stream().collect(Collectors.toMap(e -> keyMapper.apply(e.getKey()), Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<String, String> getQueryMap(String query) {
|
public static Map<String, String> getQueryMap(String query) {
|
||||||
var params = query.split("&");
|
var params = query.split("&");
|
||||||
var map = new HashMap<String, String>();
|
var map = new HashMap<String, String>();
|
||||||
|
|
|
@ -14,6 +14,7 @@ public class Commands {
|
||||||
addCommand(new BlockCommand());
|
addCommand(new BlockCommand());
|
||||||
addCommand(new DaemonCommand());
|
addCommand(new DaemonCommand());
|
||||||
addCommand(new DeleteLocalAccountDataCommand());
|
addCommand(new DeleteLocalAccountDataCommand());
|
||||||
|
addCommand(new FinishChangeNumberCommand());
|
||||||
addCommand(new FinishLinkCommand());
|
addCommand(new FinishLinkCommand());
|
||||||
addCommand(new GetAttachmentCommand());
|
addCommand(new GetAttachmentCommand());
|
||||||
addCommand(new GetUserStatusCommand());
|
addCommand(new GetUserStatusCommand());
|
||||||
|
@ -43,6 +44,7 @@ public class Commands {
|
||||||
addCommand(new SendTypingCommand());
|
addCommand(new SendTypingCommand());
|
||||||
addCommand(new SetPinCommand());
|
addCommand(new SetPinCommand());
|
||||||
addCommand(new SubmitRateLimitChallengeCommand());
|
addCommand(new SubmitRateLimitChallengeCommand());
|
||||||
|
addCommand(new StartChangeNumberCommand());
|
||||||
addCommand(new StartLinkCommand());
|
addCommand(new StartLinkCommand());
|
||||||
addCommand(new TrustCommand());
|
addCommand(new TrustCommand());
|
||||||
addCommand(new UnblockCommand());
|
addCommand(new UnblockCommand());
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.asamk.signal.commands;
|
||||||
|
|
||||||
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
|
import org.asamk.signal.commands.exceptions.CommandException;
|
||||||
|
import org.asamk.signal.commands.exceptions.IOErrorException;
|
||||||
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||||
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
|
import org.asamk.signal.manager.api.PinLockedException;
|
||||||
|
import org.asamk.signal.output.OutputWriter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class FinishChangeNumberCommand implements JsonRpcLocalCommand {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "finishChangeNumber";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attachToSubparser(final Subparser subparser) {
|
||||||
|
subparser.help("Verify the new number using the code received via SMS or voice.");
|
||||||
|
subparser.addArgument("number").help("The new phone number in E164 format.").required(true);
|
||||||
|
subparser.addArgument("-v", "--verification-code")
|
||||||
|
.help("The verification code you received via sms or voice call.")
|
||||||
|
.required(true);
|
||||||
|
subparser.addArgument("-p", "--pin").help("The registration lock PIN, that was set by the user (Optional)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(
|
||||||
|
final Namespace ns, final Manager m, final OutputWriter outputWriter
|
||||||
|
) throws CommandException {
|
||||||
|
final var newNumber = ns.getString("number");
|
||||||
|
final var verificationCode = ns.getString("verification-code");
|
||||||
|
final var pin = ns.getString("pin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
m.finishChangeNumber(newNumber, verificationCode, pin);
|
||||||
|
} catch (PinLockedException e) {
|
||||||
|
throw new UserErrorException(
|
||||||
|
"Verification failed! This number is locked with a pin. Hours remaining until reset: "
|
||||||
|
+ (e.getTimeRemaining() / 1000 / 60 / 60)
|
||||||
|
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
|
||||||
|
} catch (IncorrectPinException e) {
|
||||||
|
throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
|
||||||
|
} catch (NotPrimaryDeviceException e) {
|
||||||
|
throw new UserErrorException("This command doesn't work on linked devices.");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to change number: %s (%s)".formatted(e.getMessage(),
|
||||||
|
e.getClass().getSimpleName()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ 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.manager.api.RateLimitException;
|
||||||
import org.asamk.signal.output.JsonWriter;
|
import org.asamk.signal.output.JsonWriter;
|
||||||
import org.asamk.signal.util.DateUtils;
|
import org.asamk.signal.util.CommandUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -69,26 +69,10 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration
|
||||||
try {
|
try {
|
||||||
m.register(voiceVerification, captcha);
|
m.register(voiceVerification, captcha);
|
||||||
} catch (RateLimitException e) {
|
} catch (RateLimitException e) {
|
||||||
String message = "Rate limit reached";
|
final var message = CommandUtil.getRateLimitMessage(e);
|
||||||
if (e.getNextAttemptTimestamp() > 0) {
|
|
||||||
message += "\nNext attempt may be tried at " + DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
|
|
||||||
}
|
|
||||||
throw new RateLimitErrorException(message, e);
|
throw new RateLimitErrorException(message, e);
|
||||||
} catch (CaptchaRequiredException e) {
|
} catch (CaptchaRequiredException e) {
|
||||||
String message;
|
final var message = CommandUtil.getCaptchaRequiredMessage(e, captcha != null);
|
||||||
if (captcha == null) {
|
|
||||||
message = """
|
|
||||||
Captcha required for verification, use --captcha CAPTCHA
|
|
||||||
To get the token, go to https://signalcaptchas.org/registration/generate.html
|
|
||||||
Check the developer tools (F12) console for a failed redirect to signalcaptcha://
|
|
||||||
Everything after signalcaptcha:// is the captcha token.""";
|
|
||||||
} else {
|
|
||||||
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);
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package org.asamk.signal.commands;
|
||||||
|
|
||||||
|
import net.sourceforge.argparse4j.impl.Arguments;
|
||||||
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
|
import org.asamk.signal.commands.exceptions.CommandException;
|
||||||
|
import org.asamk.signal.commands.exceptions.IOErrorException;
|
||||||
|
import org.asamk.signal.commands.exceptions.RateLimitErrorException;
|
||||||
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||||
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
|
import org.asamk.signal.manager.api.RateLimitException;
|
||||||
|
import org.asamk.signal.output.OutputWriter;
|
||||||
|
import org.asamk.signal.util.CommandUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class StartChangeNumberCommand implements JsonRpcLocalCommand {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "startChangeNumber";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attachToSubparser(final Subparser subparser) {
|
||||||
|
subparser.help("Change account to a new phone number with SMS or voice verification.");
|
||||||
|
subparser.addArgument("number").help("The new phone number in E164 format.").required(true);
|
||||||
|
subparser.addArgument("-v", "--voice")
|
||||||
|
.help("The verification should be done over voice, not SMS.")
|
||||||
|
.action(Arguments.storeTrue());
|
||||||
|
subparser.addArgument("--captcha")
|
||||||
|
.help("The captcha token, required if change number failed with a captcha required error.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(
|
||||||
|
final Namespace ns, final Manager m, final OutputWriter outputWriter
|
||||||
|
) throws CommandException {
|
||||||
|
final var newNumber = ns.getString("number");
|
||||||
|
final var voiceVerification = Boolean.TRUE.equals(ns.getBoolean("voice"));
|
||||||
|
final var captcha = ns.getString("captcha");
|
||||||
|
|
||||||
|
try {
|
||||||
|
m.startChangeNumber(newNumber, voiceVerification, captcha);
|
||||||
|
} catch (RateLimitException e) {
|
||||||
|
final var message = CommandUtil.getRateLimitMessage(e);
|
||||||
|
throw new RateLimitErrorException(message, e);
|
||||||
|
} catch (CaptchaRequiredException e) {
|
||||||
|
final var message = CommandUtil.getCaptchaRequiredMessage(e, captcha != null);
|
||||||
|
throw new UserErrorException(message);
|
||||||
|
} catch (NonNormalizedPhoneNumberException e) {
|
||||||
|
throw new UserErrorException("Failed to change number: " + e.getMessage(), e);
|
||||||
|
} catch (NotPrimaryDeviceException e) {
|
||||||
|
throw new UserErrorException("This command doesn't work on linked devices.");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to change number: %s (%s)".formatted(e.getMessage(),
|
||||||
|
e.getClass().getSimpleName()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import org.asamk.Signal;
|
||||||
import org.asamk.signal.DbusConfig;
|
import org.asamk.signal.DbusConfig;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.Configuration;
|
import org.asamk.signal.manager.api.Configuration;
|
||||||
import org.asamk.signal.manager.api.Contact;
|
import org.asamk.signal.manager.api.Contact;
|
||||||
import org.asamk.signal.manager.api.Device;
|
import org.asamk.signal.manager.api.Device;
|
||||||
|
@ -17,15 +18,19 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
||||||
import org.asamk.signal.manager.api.Identity;
|
import org.asamk.signal.manager.api.Identity;
|
||||||
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||||
|
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.InvalidStickerException;
|
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
import org.asamk.signal.manager.api.LastGroupAdminException;
|
||||||
import org.asamk.signal.manager.api.Message;
|
import org.asamk.signal.manager.api.Message;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
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.RateLimitException;
|
||||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||||
import org.asamk.signal.manager.api.Recipient;
|
import org.asamk.signal.manager.api.Recipient;
|
||||||
import org.asamk.signal.manager.api.RecipientAddress;
|
import org.asamk.signal.manager.api.RecipientAddress;
|
||||||
|
@ -166,6 +171,20 @@ public class DbusManagerImpl implements Manager {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startChangeNumber(
|
||||||
|
final String newNumber, final boolean voiceVerification, final String captcha
|
||||||
|
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finishChangeNumber(
|
||||||
|
final String newNumber, final String verificationCode, final String pin
|
||||||
|
) throws IncorrectPinException, PinLockedException, IOException {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregister() throws IOException {
|
public void unregister() throws IOException {
|
||||||
signal.unregister();
|
signal.unregister();
|
||||||
|
|
|
@ -2,9 +2,11 @@ package org.asamk.signal.util;
|
||||||
|
|
||||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.GroupId;
|
import org.asamk.signal.manager.api.GroupId;
|
||||||
import org.asamk.signal.manager.api.GroupIdFormatException;
|
import org.asamk.signal.manager.api.GroupIdFormatException;
|
||||||
import org.asamk.signal.manager.api.InvalidNumberException;
|
import org.asamk.signal.manager.api.InvalidNumberException;
|
||||||
|
import org.asamk.signal.manager.api.RateLimitException;
|
||||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -96,4 +98,29 @@ public class CommandUtil {
|
||||||
throw new UserErrorException("Invalid phone number '" + recipientString + "': " + e.getMessage(), e);
|
throw new UserErrorException("Invalid phone number '" + recipientString + "': " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getCaptchaRequiredMessage(final CaptchaRequiredException e, final boolean captchaProvided) {
|
||||||
|
String message;
|
||||||
|
if (!captchaProvided) {
|
||||||
|
message = """
|
||||||
|
Captcha required for verification, use --captcha CAPTCHA
|
||||||
|
To get the token, go to https://signalcaptchas.org/registration/generate.html
|
||||||
|
Check the developer tools (F12) console for a failed redirect to signalcaptcha://
|
||||||
|
Everything after signalcaptcha:// is the captcha token.""";
|
||||||
|
} else {
|
||||||
|
message = "Invalid captcha given.";
|
||||||
|
}
|
||||||
|
if (e.getNextAttemptTimestamp() > 0) {
|
||||||
|
message += "\nNext Captcha may be provided at " + DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRateLimitMessage(final RateLimitException e) {
|
||||||
|
String message = "Rate limit reached";
|
||||||
|
if (e.getNextAttemptTimestamp() > 0) {
|
||||||
|
message += "\nNext attempt may be tried at " + DateUtils.formatTimestamp(e.getNextAttemptTimestamp());
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue