Skip to content

Commit

Permalink
Implement change phone number
Browse files Browse the repository at this point in the history
Closes #1240
  • Loading branch information
AsamK committed Oct 14, 2023
1 parent 56ee173 commit 33c4e17
Show file tree
Hide file tree
Showing 15 changed files with 382 additions and 63 deletions.
14 changes: 12 additions & 2 deletions graalvm-config-dir/reflect-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@
"name":"java.util.Locale",
"methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.util.Map"
},
{
"name":"java.util.Optional",
"allDeclaredFields":true,
Expand Down Expand Up @@ -505,9 +508,15 @@
{
"name":"kotlin.collections.List"
},
{
"name":"kotlin.collections.Map"
},
{
"name":"kotlin.collections.MutableList"
},
{
"name":"kotlin.collections.MutableMap"
},
{
"name":"kotlin.jvm.JvmStatic",
"queryAllDeclaredMethods":true
Expand Down Expand Up @@ -545,7 +554,7 @@
"name":"org.asamk.Signal",
"allDeclaredMethods":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",
Expand Down Expand Up @@ -2049,9 +2058,10 @@
{
"name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":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",
Expand Down
13 changes: 13 additions & 0 deletions lib/src/main/java/org/asamk/signal/manager/Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.asamk.signal.manager.api.AlreadyReceivingException;
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.Device;
import org.asamk.signal.manager.api.DeviceLinkUrl;
Expand All @@ -13,16 +14,20 @@
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.IdentityVerificationCode;
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.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException;
import org.asamk.signal.manager.api.Message;
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.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair;
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.Recipient;
import org.asamk.signal.manager.api.RecipientIdentifier;
Expand Down Expand Up @@ -107,6 +112,14 @@ static boolean isSignalClientAvailable() {
*/
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 deleteAccount() throws IOException;
Expand Down
149 changes: 133 additions & 16 deletions lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,42 @@
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.IdentityKeyPair;
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.SignedPreKeyRecord;
import org.signal.libsignal.protocol.util.KeyHelper;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.PNI;
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.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;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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;

public class AccountHelper {
Expand Down Expand Up @@ -139,7 +148,7 @@ public void setPni(
}

public void startChangeNumber(
String newNumber, String captcha, boolean voiceVerification
String newNumber, boolean voiceVerification, String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
Expand All @@ -153,20 +162,100 @@ public void startChangeNumber(
public void finishChangeNumber(
String newNumber, String verificationCode, String pin
) throws IncorrectPinException, PinLockedException, IOException {
// TODO create new PNI identity key
final List<OutgoingPushMessage> deviceMessages = null;
final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
final Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys = null;
final Map<String, Integer> pniRegistrationIds = null;
var sessionId = account.getSessionId(account.getNumber());
for (var attempts = 0; attempts < 5; attempts++) {
try {
finishChangeNumberInternal(newNumber, verificationCode, pin);
break;
} catch (MismatchedDevicesException e) {
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,
verificationCode,
pin,
context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> {
final var accountManager = dependencies.getAccountManager();
try {
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
Utils.handleResponseException(accountManager.verifyAccount(verificationCode1, sessionId1));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number
}
Expand All @@ -175,14 +264,42 @@ public void finishChangeNumber(
null,
newNumber,
registrationLock,
account.getPniIdentityKeyPair().getPublicKey(),
deviceMessages,
devicePniSignedPreKeys,
devicePniLastResortKyberPrekeys,
pniRegistrationIds)));
pniIdentity.getPublicKey(),
encryptedDeviceMessages,
Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,9 @@
import org.signal.libsignal.metadata.ProtocolNoSessionException;
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.KyberPreKeyRecord;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
Expand All @@ -67,7 +64,6 @@
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
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.internal.push.Envelope;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
Expand Down Expand Up @@ -618,24 +614,10 @@ private List<HandleAction> handleSyncMessage(
if (syncMessage.getPniChangeNumber().isPresent()) {
final var pniChangeNumber = syncMessage.getPniChangeNumber().get();
logger.debug("Received PNI change number sync message, applying.");
if (pniChangeNumber.identityKeyPair != null
&& pniChangeNumber.registrationId != null
&& pniChangeNumber.signedPreKey != null
&& !envelope.getUpdatedPni().isEmpty()) {
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);
}
final var updatedPniString = envelope.getUpdatedPni();
if (updatedPniString != null && !updatedPniString.isEmpty()) {
final var updatedPni = ServiceId.PNI.parseOrThrow(updatedPniString);
context.getAccountHelper().handlePniChangeNumberMessage(pniChangeNumber, updatedPni);
}
}
return actions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ public void refreshPreKeysIfNecessary(ServiceIdType serviceIdType) throws IOExce
preKeyRecords,
lastResortKyberPreKeyRecord,
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));
Expand Down
Loading

0 comments on commit 33c4e17

Please sign in to comment.