parent
							
								
									56ee173d03
								
							
						
					
					
						commit
						33c4e17c0d
					
				| @ -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, | ||||
| @ -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 | ||||
| @ -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", | ||||
| @ -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", | ||||
|  | ||||
| @ -2,6 +2,7 @@ package org.asamk.signal.manager; | ||||
| 
 | ||||
| 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; | ||||
| @ -13,16 +14,20 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException; | ||||
| 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; | ||||
| @ -107,6 +112,14 @@ public interface Manager extends Closeable { | ||||
|      */ | ||||
|     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; | ||||
|  | ||||
| @ -14,16 +14,20 @@ 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.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; | ||||
| @ -31,16 +35,21 @@ import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionExc | ||||
| 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 { | ||||
| @ -139,7 +148,7 @@ public class AccountHelper { | ||||
|     } | ||||
| 
 | ||||
|     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, | ||||
| @ -153,12 +162,92 @@ public class AccountHelper { | ||||
|     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, | ||||
| @ -166,7 +255,7 @@ public class AccountHelper { | ||||
|                 (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 | ||||
|                     } | ||||
| @ -175,14 +264,42 @@ public class AccountHelper { | ||||
|                             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; | ||||
|  | ||||
| @ -40,12 +40,9 @@ import org.signal.libsignal.metadata.ProtocolInvalidMessageException; | ||||
| 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; | ||||
| @ -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.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; | ||||
| @ -618,24 +614,10 @@ public final class IncomingMessageHandler { | ||||
|         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; | ||||
|  | ||||
| @ -112,7 +112,12 @@ public class PreKeyHelper { | ||||
|                     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)); | ||||
|  | ||||
| @ -19,6 +19,7 @@ package org.asamk.signal.manager.internal; | ||||
| import org.asamk.signal.manager.Manager; | ||||
| 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; | ||||
| @ -30,17 +31,21 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException; | ||||
| 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.Profile; | ||||
| 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; | ||||
| @ -317,6 +322,26 @@ public class ManagerImpl implements Manager { | ||||
|         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 | ||||
|     public void unregister() throws IOException { | ||||
|         context.getAccountHelper().unregister(); | ||||
|  | ||||
| @ -1383,7 +1383,6 @@ public class SignalAccount implements Closeable { | ||||
|         if (oldPni != null && !oldPni.equals(updatedPni)) { | ||||
|             // Clear data for old PNI | ||||
|             identityKeyStore.deleteIdentity(oldPni); | ||||
|             clearAllPreKeys(ServiceIdType.PNI); | ||||
|         } | ||||
| 
 | ||||
|         this.pniAccountData.setServiceId(updatedPni); | ||||
| @ -1400,11 +1399,14 @@ public class SignalAccount implements Closeable { | ||||
|         setPniIdentityKeyPair(pniIdentityKeyPair); | ||||
|         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(); | ||||
|         accountData.getSignedPreKeyStore().removeSignedPreKey(pniSignedPreKey.getId()); | ||||
|         addSignedPreKey(ServiceIdType.PNI, pniSignedPreKey); | ||||
|         if (lastResortKyberPreKey != null) { | ||||
|             preKeyMetadata.nextKyberPreKeyId = lastResortKyberPreKey.getId(); | ||||
|             accountData.getKyberPreKeyStore().removeKyberPreKey(lastResortKyberPreKey.getId()); | ||||
|             addLastResortKyberPreKey(ServiceIdType.PNI, lastResortKyberPreKey); | ||||
|         } | ||||
|         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.RateLimitException; | ||||
| 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.kbs.MasterKey; | ||||
| import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException; | ||||
| @ -24,6 +26,8 @@ import java.util.function.Consumer; | ||||
| 
 | ||||
| public class NumberVerificationUtils { | ||||
| 
 | ||||
|     private final static Logger logger = LoggerFactory.getLogger(NumberVerificationUtils.class); | ||||
| 
 | ||||
|     public static String handleVerificationSession( | ||||
|             SignalServiceAccountManager accountManager, | ||||
|             String sessionId, | ||||
| @ -143,7 +147,7 @@ public class NumberVerificationUtils { | ||||
| 
 | ||||
|     private static RegistrationSessionMetadataResponse requestValidSession( | ||||
|             final SignalServiceAccountManager accountManager | ||||
|     ) throws NoSuchSessionException, IOException { | ||||
|     ) throws IOException { | ||||
|         return Utils.handleResponseException(accountManager.createRegistrationSession(null, "", "")); | ||||
|     } | ||||
| 
 | ||||
| @ -153,6 +157,7 @@ public class NumberVerificationUtils { | ||||
|         try { | ||||
|             return validateSession(accountManager, sessionId); | ||||
|         } catch (NoSuchSessionException e) { | ||||
|             logger.debug("No registration session, creating new one."); | ||||
|             return requestValidSession(accountManager); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -25,6 +25,8 @@ import java.util.Spliterator; | ||||
| import java.util.Spliterators; | ||||
| import java.util.function.BiFunction; | ||||
| import java.util.function.Consumer; | ||||
| import java.util.function.Function; | ||||
| import java.util.stream.Collectors; | ||||
| import java.util.stream.Stream; | ||||
| import java.util.stream.StreamSupport; | ||||
| 
 | ||||
| @ -122,6 +124,10 @@ public class Utils { | ||||
|         }, 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) { | ||||
|         var params = query.split("&"); | ||||
|         var map = new HashMap<String, String>(); | ||||
|  | ||||
| @ -14,6 +14,7 @@ public class Commands { | ||||
|         addCommand(new BlockCommand()); | ||||
|         addCommand(new DaemonCommand()); | ||||
|         addCommand(new DeleteLocalAccountDataCommand()); | ||||
|         addCommand(new FinishChangeNumberCommand()); | ||||
|         addCommand(new FinishLinkCommand()); | ||||
|         addCommand(new GetAttachmentCommand()); | ||||
|         addCommand(new GetUserStatusCommand()); | ||||
| @ -43,6 +44,7 @@ public class Commands { | ||||
|         addCommand(new SendTypingCommand()); | ||||
|         addCommand(new SetPinCommand()); | ||||
|         addCommand(new SubmitRateLimitChallengeCommand()); | ||||
|         addCommand(new StartChangeNumberCommand()); | ||||
|         addCommand(new StartLinkCommand()); | ||||
|         addCommand(new TrustCommand()); | ||||
|         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.RateLimitException; | ||||
| import org.asamk.signal.output.JsonWriter; | ||||
| import org.asamk.signal.util.DateUtils; | ||||
| import org.asamk.signal.util.CommandUtil; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| @ -69,26 +69,10 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration | ||||
|         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()); | ||||
|             } | ||||
|             final var message = CommandUtil.getRateLimitMessage(e); | ||||
|             throw new RateLimitErrorException(message, e); | ||||
|         } catch (CaptchaRequiredException e) { | ||||
|             String message; | ||||
|             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()); | ||||
|             } | ||||
|             final var message = CommandUtil.getCaptchaRequiredMessage(e, captcha != null); | ||||
|             throw new UserErrorException(message); | ||||
|         } catch (NonNormalizedPhoneNumberException 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.manager.Manager; | ||||
| 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.Contact; | ||||
| 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.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.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.RecipientAddress; | ||||
| @ -166,6 +171,20 @@ public class DbusManagerImpl implements Manager { | ||||
|         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 | ||||
|     public void unregister() throws IOException { | ||||
|         signal.unregister(); | ||||
|  | ||||
| @ -2,9 +2,11 @@ package org.asamk.signal.util; | ||||
| 
 | ||||
| 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.GroupId; | ||||
| import org.asamk.signal.manager.api.GroupIdFormatException; | ||||
| import org.asamk.signal.manager.api.InvalidNumberException; | ||||
| import org.asamk.signal.manager.api.RateLimitException; | ||||
| import org.asamk.signal.manager.api.RecipientIdentifier; | ||||
| 
 | ||||
| import java.util.Collection; | ||||
| @ -96,4 +98,29 @@ public class CommandUtil { | ||||
|             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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 AsamK
						AsamK