44 */
55package org .whispersystems .textsecuregcm .storage ;
66
7+ import static org .junit .jupiter .api .Assertions .assertEquals ;
78import static org .junit .jupiter .api .Assertions .assertThrows ;
89import static org .mockito .ArgumentMatchers .any ;
910import static org .mockito .ArgumentMatchers .anyByte ;
2223import jakarta .ws .rs .container .ContainerRequestContext ;
2324import java .time .Duration ;
2425import java .time .Instant ;
26+ import java .time .temporal .ChronoUnit ;
27+ import java .util .Collection ;
2528import java .util .Collections ;
2629import java .util .HashMap ;
2730import java .util .List ;
3033import java .util .UUID ;
3134import org .junit .jupiter .api .BeforeEach ;
3235import org .junit .jupiter .api .Test ;
36+ import org .junit .jupiter .api .function .Executable ;
37+ import org .junit .jupiter .params .ParameterizedTest ;
38+ import org .junit .jupiter .params .provider .Arguments ;
39+ import org .junit .jupiter .params .provider .MethodSource ;
3340import org .mockito .stubbing .Answer ;
3441import org .signal .libsignal .protocol .IdentityKey ;
3542import org .signal .libsignal .protocol .ecc .ECKeyPair ;
@@ -67,6 +74,14 @@ public class ChangeNumberManagerTest {
6774
6875 private static final TestClock CLOCK = TestClock .pinned (Instant .now ());
6976
77+ private static final Duration POST_REGISTRATION_WAITING_PERIOD = Duration .ofHours (2 );
78+ private static final Device DEFAULT_PRIMARY_DEVICE ;
79+ static {
80+ DEFAULT_PRIMARY_DEVICE = new Device ();
81+ DEFAULT_PRIMARY_DEVICE .setId ((byte ) 1 );
82+ DEFAULT_PRIMARY_DEVICE .setCreated (CLOCK .instant ().minus (POST_REGISTRATION_WAITING_PERIOD ).minusSeconds (1 ).toEpochMilli ());
83+ }
84+
7085 @ BeforeEach
7186 void setUp () throws Exception {
7287 messageSender = mock (MessageSender .class );
@@ -87,7 +102,8 @@ void setUp() throws Exception {
87102 when (rateLimiters .getRegistrationLimiter ()).thenReturn (rateLimiter );
88103
89104 changeNumberManager = new ChangeNumberManager (messageSender , accountsManager ,
90- phoneVerificationTokenManager , registrationLockVerificationManager , rateLimiters , CLOCK );
105+ phoneVerificationTokenManager , registrationLockVerificationManager , rateLimiters ,
106+ POST_REGISTRATION_WAITING_PERIOD , CLOCK );
91107
92108 updatedPhoneNumberIdentifiersByAccount = new HashMap <>();
93109
@@ -100,6 +116,7 @@ void setUp() throws Exception {
100116
101117 final UUID uuid = account .getIdentifier (IdentityType .ACI );
102118 final List <Device > devices = account .getDevices ();
119+ final Device primaryDevice = account .getPrimaryDevice ();
103120
104121 final UUID updatedPni = UUID .randomUUID ();
105122 updatedPhoneNumberIdentifiersByAccount .put (account , updatedPni );
@@ -113,6 +130,7 @@ void setUp() throws Exception {
113130 when (updatedAccount .getNumber ()).thenReturn (number );
114131 when (updatedAccount .getDevices ()).thenReturn (devices );
115132 when (updatedAccount .getDevice (anyByte ())).thenReturn (Optional .empty ());
133+ when (updatedAccount .getPrimaryDevice ()).thenReturn (primaryDevice );
116134
117135 account .getDevices ().forEach (device ->
118136 when (updatedAccount .getDevice (device .getId ())).thenReturn (Optional .of (device )));
@@ -145,6 +163,7 @@ void changeNumberSingleDevice() throws Exception {
145163 when (account .getIdentifier (IdentityType .ACI )).thenReturn (accountIdentifier );
146164 when (account .isIdentifiedBy (any ())).thenReturn (false );
147165 when (account .isIdentifiedBy (new AciServiceIdentifier (accountIdentifier ))).thenReturn (true );
166+ when (account .getPrimaryDevice ()).thenReturn (DEFAULT_PRIMARY_DEVICE );
148167
149168 when (accountsManager .getAccountsForChangeNumber (eq (accountIdentifier ), any ()))
150169 .thenReturn (new Pair <>(account , Optional .empty ()));
@@ -187,6 +206,7 @@ void changeNumberLinkedDevices() throws Exception {
187206 when (account .getDevice (primaryDeviceId )).thenReturn (Optional .of (primaryDevice ));
188207 when (account .getDevice (linkedDeviceId )).thenReturn (Optional .of (linkedDevice ));
189208 when (account .getDevices ()).thenReturn (List .of (primaryDevice , linkedDevice ));
209+ when (account .getPrimaryDevice ()).thenReturn (DEFAULT_PRIMARY_DEVICE );
190210
191211 final ECKeyPair pniIdentityKeyPair = ECKeyPair .generate ();
192212 final IdentityKey pniIdentityKey = new IdentityKey (pniIdentityKeyPair .getPublicKey ());
@@ -269,6 +289,7 @@ void changeNumberSameNumber() throws Exception {
269289 when (account .getIdentifier (IdentityType .ACI )).thenReturn (accountIdentifier );
270290 when (account .isIdentifiedBy (any ())).thenReturn (false );
271291 when (account .isIdentifiedBy (new AciServiceIdentifier (accountIdentifier ))).thenReturn (true );
292+ when (account .getPrimaryDevice ()).thenReturn (DEFAULT_PRIMARY_DEVICE );
272293
273294 when (accountsManager .getAccountsForChangeNumber (eq (accountIdentifier ), any ()))
274295 .thenReturn (new Pair <>(account , Optional .empty ()));
@@ -308,6 +329,7 @@ void changeNumberRateLimited()
308329 when (account .getIdentifier (IdentityType .ACI )).thenReturn (accountIdentifier );
309330 when (account .isIdentifiedBy (any ())).thenReturn (false );
310331 when (account .isIdentifiedBy (new AciServiceIdentifier (accountIdentifier ))).thenReturn (true );
332+ when (account .getPrimaryDevice ()).thenReturn (DEFAULT_PRIMARY_DEVICE );
311333
312334 when (accountsManager .getAccountsForChangeNumber (eq (accountIdentifier ), any ()))
313335 .thenReturn (new Pair <>(account , Optional .empty ()));
@@ -356,6 +378,7 @@ void changeNumberRegistrationLockFailed()
356378 when (account .getIdentifier (IdentityType .ACI )).thenReturn (accountIdentifier );
357379 when (account .isIdentifiedBy (any ())).thenReturn (false );
358380 when (account .isIdentifiedBy (new AciServiceIdentifier (accountIdentifier ))).thenReturn (true );
381+ when (account .getPrimaryDevice ()).thenReturn (DEFAULT_PRIMARY_DEVICE );
359382
360383 final Account existingAccount = mock (Account .class );
361384 when (existingAccount .getNumber ()).thenReturn (targetNumber );
@@ -381,4 +404,75 @@ void changeNumberRegistrationLockFailed()
381404 verify (accountsManager , never ()).changeNumber (any (), any (), any (), any (), any (), any ());
382405 verify (messageSender , never ()).sendMessages (any (), any (), any (), any (), any (), any ());
383406 }
407+
408+ @ ParameterizedTest
409+ @ MethodSource
410+ void testRecentRegistration (final boolean expectRateLimited , final boolean sameNumber , final Instant registrationInstant ) throws Throwable {
411+
412+ final String originalNumber = PhoneNumberUtil .getInstance ().format (
413+ PhoneNumberUtil .getInstance ().getExampleNumber ("DE" ), PhoneNumberUtil .PhoneNumberFormat .E164 );
414+
415+ final String targetNumber = sameNumber
416+ ? originalNumber
417+ : PhoneNumberUtil .getInstance ().format (
418+ PhoneNumberUtil .getInstance ().getExampleNumber ("US" ), PhoneNumberUtil .PhoneNumberFormat .E164 );
419+
420+ final ECKeyPair pniIdentityKeyPair = ECKeyPair .generate ();
421+ final IdentityKey pniIdentityKey = new IdentityKey (ECKeyPair .generate ().getPublicKey ());
422+
423+ final Map <Byte , ECSignedPreKey > ecSignedPreKeys =
424+ Map .of (Device .PRIMARY_ID , KeysHelper .signedECPreKey (1 , pniIdentityKeyPair ));
425+
426+ final Map <Byte , KEMSignedPreKey > kemLastResortPreKeys =
427+ Map .of (Device .PRIMARY_ID , KeysHelper .signedKEMPreKey (2 , pniIdentityKeyPair ));
428+
429+ final UUID accountIdentifier = UUID .randomUUID ();
430+
431+ final Account account = mock (Account .class );
432+ when (account .getNumber ()).thenReturn (originalNumber );
433+ when (account .getIdentifier (IdentityType .ACI )).thenReturn (accountIdentifier );
434+ when (account .isIdentifiedBy (any ())).thenReturn (false );
435+ when (account .isIdentifiedBy (new AciServiceIdentifier (accountIdentifier ))).thenReturn (true );
436+
437+ final Device primaryDevice = mock (Device .class );
438+ when (account .getPrimaryDevice ()).thenReturn (primaryDevice );
439+ when (primaryDevice .getCreated ()).thenReturn (registrationInstant .toEpochMilli ());
440+
441+ when (accountsManager .getAccountsForChangeNumber (eq (accountIdentifier ), any ()))
442+ .thenReturn (new Pair <>(account , Optional .empty ()));
443+
444+ final Executable changeNumberOperation = () -> changeNumberManager .changeNumber (accountIdentifier ,
445+ null ,
446+ null ,
447+ null ,
448+ targetNumber ,
449+ pniIdentityKey ,
450+ ecSignedPreKeys ,
451+ kemLastResortPreKeys ,
452+ Collections .emptyList (),
453+ Collections .emptyMap (),
454+ mock (ContainerRequestContext .class ));
455+ if (expectRateLimited ) {
456+ final RateLimitExceededException e = assertThrows (RateLimitExceededException .class , changeNumberOperation );
457+
458+ assertEquals (Duration .between (CLOCK .instant ().minus (POST_REGISTRATION_WAITING_PERIOD ), registrationInstant ), e .getRetryDuration ().orElseThrow ());
459+ } else {
460+ changeNumberOperation .execute ();
461+ verify (accountsManager ).changeNumber (accountIdentifier , targetNumber , pniIdentityKey , ecSignedPreKeys , kemLastResortPreKeys , Collections .emptyMap ());
462+ }
463+ }
464+
465+ static Collection <Arguments > testRecentRegistration () {
466+ // truncate to millis because that is the resolution for device.created
467+ final Instant tooRecent = CLOCK .instant ().minus (POST_REGISTRATION_WAITING_PERIOD ).plusSeconds (1 )
468+ .truncatedTo (ChronoUnit .MILLIS );
469+ final Instant outsideWaitingPeriod = CLOCK .instant ().minus (POST_REGISTRATION_WAITING_PERIOD ).minusSeconds (1 )
470+ .truncatedTo (ChronoUnit .MILLIS );
471+ return List .of (
472+ // expect exception, same number, registration instant
473+ Arguments .argumentSet ("waiting period elapsed" , false , false , outsideWaitingPeriod ),
474+ Arguments .argumentSet ("waiting period not elapsed" , true , false , tooRecent ),
475+ Arguments .argumentSet ("waiting period not elapsed; same number" , false , true , tooRecent )
476+ );
477+ }
384478}
0 commit comments