Skip to content

Commit cbace93

Browse files
committed
Use constant-time comparison of verification codes
Rate limits should make timing attacks or brute-forcing impossible regardless
1 parent 1503fbc commit cbace93

11 files changed

Lines changed: 74 additions & 13 deletions

File tree

src/main/java/org/signal/registration/sender/LastDigitsOfPhoneNumberVerificationCodeSender.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.Locale;
1717
import java.util.Optional;
1818
import org.signal.registration.Environments;
19+
import org.signal.registration.util.Strings;
1920

2021
/**
2122
* A trivial verification code "sender" that never actually sends codes, but instead always uses the last six digits of
@@ -60,7 +61,7 @@ public AttemptData sendVerificationCode(final MessageTransport messageTransport,
6061

6162
@Override
6263
public boolean checkVerificationCode(final String verificationCode, final byte[] senderData) {
63-
return verificationCode.equals(new String(senderData, StandardCharsets.UTF_8));
64+
return Strings.equalsConstantTime(verificationCode, new String(senderData, StandardCharsets.UTF_8));
6465
}
6566

6667
public static String getVerificationCode(final Phonenumber.PhoneNumber phoneNumber) {

src/main/java/org/signal/registration/sender/fictitious/FictitiousNumberVerificationCodeSender.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.signal.registration.sender.MessageTransport;
2222
import org.signal.registration.sender.VerificationCodeGenerator;
2323
import org.signal.registration.sender.VerificationCodeSender;
24+
import org.signal.registration.util.Strings;
2425
import org.slf4j.Logger;
2526
import org.slf4j.LoggerFactory;
2627

@@ -107,7 +108,7 @@ public boolean checkVerificationCode(final String verificationCode, final byte[]
107108
final String expectedVerificationCode =
108109
FictitiousNumberVerificationCodeSessionData.parseFrom(senderData).getVerificationCode();
109110

110-
return expectedVerificationCode.equals(verificationCode);
111+
return Strings.equalsConstantTime(expectedVerificationCode, verificationCode);
111112
} catch (final InvalidProtocolBufferException e) {
112113
logger.error("Failed to parse stored session data", e);
113114
throw new UncheckedIOException(e);

src/main/java/org/signal/registration/sender/infobip/classic/InfobipSmsSender.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.Locale;
2525
import java.util.Optional;
2626
import javax.annotation.Nullable;
27-
import org.apache.commons.lang3.StringUtils;
2827
import org.signal.registration.sender.ApiClientInstrumenter;
2928
import org.signal.registration.sender.AttemptData;
3029
import org.signal.registration.sender.ClientType;
@@ -39,6 +38,7 @@
3938
import org.signal.registration.sender.infobip.InfobipClassicSessionData;
4039
import org.signal.registration.sender.infobip.InfobipExceptions;
4140
import org.signal.registration.sender.infobip.InfobipSenderConfiguration;
41+
import org.signal.registration.util.Strings;
4242
import org.slf4j.Logger;
4343
import org.slf4j.LoggerFactory;
4444

@@ -195,7 +195,7 @@ private static String checkSenderRejectedAndExtractMessageId(@Nullable final Lis
195195
public boolean checkVerificationCode(final String verificationCode, final byte[] senderData) {
196196
try {
197197
final String storedVerificationCode = InfobipClassicSessionData.parseFrom(senderData).getVerificationCode();
198-
return StringUtils.equals(verificationCode, storedVerificationCode);
198+
return Strings.equalsConstantTime(verificationCode, storedVerificationCode);
199199
} catch (final InvalidProtocolBufferException e) {
200200
logger.error("Failed to parse stored session data", e);
201201
throw new UncheckedIOException(e);

src/main/java/org/signal/registration/sender/messagebird/classic/MessageBirdSmsSender.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.List;
2222
import java.util.Locale;
2323
import java.util.Optional;
24-
import org.apache.commons.lang3.StringUtils;
2524
import org.signal.registration.sender.ApiClientInstrumenter;
2625
import org.signal.registration.sender.AttemptData;
2726
import org.signal.registration.sender.ClientType;
@@ -35,6 +34,7 @@
3534
import org.signal.registration.sender.messagebird.MessageBirdClassicSessionData;
3635
import org.signal.registration.sender.messagebird.MessageBirdExceptions;
3736
import org.signal.registration.sender.messagebird.MessageBirdSenderConfiguration;
37+
import org.signal.registration.util.Strings;
3838
import org.slf4j.Logger;
3939
import org.slf4j.LoggerFactory;
4040

@@ -167,7 +167,7 @@ public AttemptData sendVerificationCode(final MessageTransport messageTransport,
167167
public boolean checkVerificationCode(final String verificationCode, final byte[] senderData) {
168168
try {
169169
final String storedVerificationCode = MessageBirdClassicSessionData.parseFrom(senderData).getVerificationCode();
170-
return StringUtils.equals(verificationCode, storedVerificationCode);
170+
return Strings.equalsConstantTime(verificationCode, storedVerificationCode);
171171
} catch (final InvalidProtocolBufferException e) {
172172
logger.error("Failed to parse stored session data", e);
173173
throw new UncheckedIOException(e);

src/main/java/org/signal/registration/sender/messagebird/classic/MessageBirdVoiceSender.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.util.List;
2020
import java.util.Locale;
2121
import java.util.Optional;
22-
import org.apache.commons.lang3.StringUtils;
2322
import org.signal.registration.sender.ApiClientInstrumenter;
2423
import org.signal.registration.sender.AttemptData;
2524
import org.signal.registration.sender.ClientType;
@@ -32,6 +31,7 @@
3231
import org.signal.registration.sender.messagebird.MessageBirdClassicSessionData;
3332
import org.signal.registration.sender.messagebird.MessageBirdExceptions;
3433
import org.signal.registration.sender.messagebird.MessageBirdSenderConfiguration;
34+
import org.signal.registration.util.Strings;
3535
import org.slf4j.Logger;
3636
import org.slf4j.LoggerFactory;
3737

@@ -163,7 +163,7 @@ public AttemptData sendVerificationCode(final MessageTransport messageTransport,
163163
public boolean checkVerificationCode(final String verificationCode, final byte[] sessionData) {
164164
try {
165165
final String storedVerificationCode = MessageBirdClassicSessionData.parseFrom(sessionData).getVerificationCode();
166-
return StringUtils.equals(verificationCode, storedVerificationCode);
166+
return Strings.equalsConstantTime(verificationCode, storedVerificationCode);
167167
} catch (final InvalidProtocolBufferException e) {
168168
logger.error("Failed to parse stored session data", e);
169169
throw new UncheckedIOException(e);

src/main/java/org/signal/registration/sender/prescribed/PrescribedVerificationCodeSender.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.signal.registration.sender.MessageTransport;
2626
import org.signal.registration.sender.SenderRejectedRequestException;
2727
import org.signal.registration.sender.VerificationCodeSender;
28+
import org.signal.registration.util.Strings;
2829
import org.slf4j.Logger;
2930
import org.slf4j.LoggerFactory;
3031

@@ -102,7 +103,7 @@ public boolean checkVerificationCode(final String verificationCode, final byte[]
102103
final String expectedVerificationCode =
103104
PrescribedVerificationCodeSessionData.parseFrom(senderData).getVerificationCode();
104105

105-
return StringUtils.equals(verificationCode, expectedVerificationCode);
106+
return Strings.equalsConstantTime(verificationCode, expectedVerificationCode);
106107
} catch (final InvalidProtocolBufferException e) {
107108
logger.error("Failed to parse stored session data", e);
108109
throw new UncheckedIOException(e);

src/main/java/org/signal/registration/sender/sinch/classic/SinchSmsSender.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.signal.registration.sender.sinch.SinchClassicSessionData;
3636
import org.signal.registration.sender.sinch.SinchExceptions;
3737
import org.signal.registration.sender.sinch.SinchSenderConfiguration;
38+
import org.signal.registration.util.Strings;
3839
import org.slf4j.Logger;
3940
import org.slf4j.LoggerFactory;
4041

@@ -148,7 +149,7 @@ public boolean checkVerificationCode(final String verificationCode, final byte[]
148149
throws SenderRejectedRequestException {
149150
try {
150151
final String storedVerificationCode = SinchClassicSessionData.parseFrom(senderData).getVerificationCode();
151-
return storedVerificationCode.equals(verificationCode);
152+
return Strings.equalsConstantTime(storedVerificationCode, verificationCode);
152153
} catch (final InvalidProtocolBufferException e) {
153154
logger.error("Failed to parse stored session data", e);
154155
throw new UncheckedIOException(e);

src/main/java/org/signal/registration/sender/twilio/classic/AbstractTwilioProvidedCodeSender.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
import com.twilio.type.PhoneNumber;
1212
import java.io.UncheckedIOException;
1313
import java.util.Optional;
14-
import org.apache.commons.lang3.StringUtils;
1514
import org.signal.registration.sender.AttemptData;
1615
import org.signal.registration.sender.VerificationCodeSender;
16+
import org.signal.registration.util.Strings;
1717
import org.slf4j.Logger;
1818
import org.slf4j.LoggerFactory;
1919

@@ -39,7 +39,7 @@ protected static PhoneNumber twilioNumberFromPhoneNumber(final Phonenumber.Phone
3939
@Override
4040
public boolean checkVerificationCode(final String verificationCode, final byte[] senderData) {
4141
try {
42-
return StringUtils.equals(verificationCode,
42+
return Strings.equalsConstantTime(verificationCode,
4343
TwilioProvidedCodeSessionData.parseFrom(senderData).getVerificationCode());
4444
} catch (final InvalidProtocolBufferException e) {
4545
logger.error("Failed to parse stored session data", e);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2026 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.signal.registration.util;
7+
8+
import java.nio.charset.StandardCharsets;
9+
import java.security.MessageDigest;
10+
11+
public class Strings {
12+
13+
private Strings() {
14+
}
15+
16+
/**
17+
* Compares two strings in constant time
18+
*
19+
* @see java.security.MessageDigest#isEqual(byte[], byte[])
20+
*/
21+
public static boolean equalsConstantTime(final String s1, final String s2) {
22+
return MessageDigest.isEqual(s1.getBytes(StandardCharsets.UTF_8), s2.getBytes(StandardCharsets.UTF_8));
23+
}
24+
25+
}

src/test/java/org/signal/registration/sender/TestVerificationCodeSender.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.signal.registration.sender;
22

33
import com.google.i18n.phonenumbers.Phonenumber;
4+
import org.signal.registration.util.Strings;
45
import java.nio.charset.StandardCharsets;
56
import java.time.Duration;
67
import java.util.List;
@@ -56,6 +57,6 @@ public AttemptData sendVerificationCode(
5657

5758
@Override
5859
public boolean checkVerificationCode(final String verificationCode, final byte[] senderData) {
59-
return verificationCode.equals(new String(senderData, StandardCharsets.UTF_8));
60+
return Strings.equalsConstantTime(verificationCode, new String(senderData, StandardCharsets.UTF_8));
6061
}
6162
}

0 commit comments

Comments
 (0)