Local SMS rate limiting.

This commit is contained in:
Alan Evans 2019-10-17 13:57:30 -04:00 committed by Greyson Parrelli
parent 8c037456e7
commit 16f41555ba
4 changed files with 210 additions and 7 deletions

View File

@ -203,6 +203,14 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
NavController navController = Navigation.findNavController(register); NavController navController = Navigation.findNavController(register);
if (!model.getRequestLimiter().canRequest(mode, e164number, System.currentTimeMillis())) {
Log.i(TAG, "Local rate limited");
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
cancelSpinning(register);
enableAllEntries();
return;
}
RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret()); RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret());
registrationService.requestVerificationCode(requireActivity(), mode, captcha, registrationService.requestVerificationCode(requireActivity(), mode, captcha,
@ -213,6 +221,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha()); navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
cancelSpinning(register); cancelSpinning(register);
enableAllEntries(); enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
} }
@Override @Override
@ -222,6 +232,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
cancelSpinning(register); cancelSpinning(register);
enableAllEntries(); enableAllEntries();
model.getRequestLimiter().onSuccessfulRequest(mode, e164number, System.currentTimeMillis());
model.updateLimiter();
} }
@Override @Override
@ -229,6 +241,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show(); Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
cancelSpinning(register); cancelSpinning(register);
enableAllEntries(); enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
} }
}); });
} }

View File

@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public final class LocalCodeRequestRateLimiter implements Parcelable {
private final long timePeriod;
private final Map<RegistrationCodeRequest.Mode, Data> dataMap;
public LocalCodeRequestRateLimiter(long timePeriod) {
this.timePeriod = timePeriod;
this.dataMap = new HashMap<>();
}
@MainThread
public boolean canRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
Data data = dataMap.get(mode);
return data == null || !data.limited(e164Number, currentTime);
}
/**
* Call this when the server has returned that it was successful in requesting a code via the specified mode.
*/
@MainThread
public void onSuccessfulRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
dataMap.put(mode, new Data(e164Number, currentTime + timePeriod));
}
/**
* Call this if a mode was unsuccessful in sending.
*/
@MainThread
public void onUnsuccessfulRequest() {
dataMap.clear();
}
static class Data {
final String e164Number;
final long limitedUntil;
Data(@NonNull String e164Number, long limitedUntil) {
this.e164Number = e164Number;
this.limitedUntil = limitedUntil;
}
boolean limited(String e164Number, long currentTime) {
return this.e164Number.equals(e164Number) && currentTime < limitedUntil;
}
}
public static final Creator<LocalCodeRequestRateLimiter> CREATOR = new Creator<LocalCodeRequestRateLimiter>() {
@Override
public LocalCodeRequestRateLimiter createFromParcel(Parcel in) {
long timePeriod = in.readLong();
int numberOfMapEntries = in.readInt();
LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod);
for (int i = 0; i < numberOfMapEntries; i++) {
RegistrationCodeRequest.Mode mode = RegistrationCodeRequest.Mode.values()[in.readInt()];
String e164Number = in.readString();
long limitedUntil = in.readLong();
localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil));
}
return localCodeRequestRateLimiter;
}
@Override
public LocalCodeRequestRateLimiter[] newArray(int size) {
return new LocalCodeRequestRateLimiter[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(timePeriod);
dest.writeInt(dataMap.size());
for (Map.Entry<RegistrationCodeRequest.Mode, Data> a : dataMap.entrySet()) {
dest.writeInt(a.getKey().ordinal());
dest.writeString(a.getValue().e164Number);
dest.writeLong(a.getValue().limitedUntil);
}
}
}

View File

@ -19,6 +19,7 @@ public final class RegistrationViewModel extends ViewModel {
private final MutableLiveData<String> fcmToken; private final MutableLiveData<String> fcmToken;
private final MutableLiveData<Boolean> restoreFlowShown; private final MutableLiveData<Boolean> restoreFlowShown;
private final MutableLiveData<Integer> successfulCodeRequestAttempts; private final MutableLiveData<Integer> successfulCodeRequestAttempts;
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) { public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18)); secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
@ -29,6 +30,7 @@ public final class RegistrationViewModel extends ViewModel {
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN"); fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false); restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0); successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000));
} }
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) { private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
@ -127,4 +129,13 @@ public final class RegistrationViewModel extends ViewModel {
public LiveData<Integer> getSuccessfulCodeRequestAttempts() { public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
return successfulCodeRequestAttempts; return successfulCodeRequestAttempts;
} }
public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
//noinspection ConstantConditions Live data was given an initial value
return requestLimiter.getValue();
}
public void updateLimiter() {
requestLimiter.setValue(requestLimiter.getValue());
}
} }

View File

@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import org.junit.Test;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class LocalCodeRequestRateLimiterTest {
@Test
public void initially_can_request() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
}
@Test
public void cant_request_within_same_time_period() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
assertFalse(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000 + 59_000));
}
@Test
public void can_request_within_same_time_period_if_different_number() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+15559874566", 1000 + 59_000));
}
@Test
public void can_request_within_same_time_period_if_different_mode() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_FCM_NO_LISTENER, "+155512345678", 1000 + 59_000));
}
@Test
public void can_request_after_time_period() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000 + 60_001));
}
@Test
public void can_request_within_same_time_period_if_an_unsuccessful_request_is_seen() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000);
limiter.onUnsuccessfulRequest();
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_NO_FCM, "+155512345678", 1000 + 59_000));
}
}