Skip to content

Commit 9fff7ca

Browse files
committed
Add methods for generating codes as strings.
1 parent e416bf6 commit 9fff7ca

File tree

7 files changed

+215
-11
lines changed

7 files changed

+215
-11
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ Armed with a key, we can deterministically generate one-time passwords for any t
4646
final Instant now = Instant.now();
4747
final Instant later = now.plus(totp.getTimeStep());
4848

49-
System.out.format("Current password: %06d\n", totp.generateOneTimePassword(key, now));
50-
System.out.format("Future password: %06d\n", totp.generateOneTimePassword(key, later));
49+
System.out.println("Current password: " + totp.generateOneTimePasswordString(key, now));
50+
System.out.println("Future password: " + totp.generateOneTimePasswordString(key, later));
5151
```
5252

5353
…which produces (for one randomly-generated key):

src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.security.InvalidKeyException;
2626
import java.security.Key;
2727
import java.security.NoSuchAlgorithmException;
28+
import java.util.Locale;
2829

2930
/**
3031
* <p>Generates HMAC-based one-time passwords (HOTP) as specified in
@@ -41,6 +42,8 @@ public class HmacOneTimePasswordGenerator {
4142
private final byte[] buffer;
4243
private final int modDivisor;
4344

45+
private final String formatString;
46+
4447
/**
4548
* The default length, in decimal digits, for one-time passwords.
4649
*/
@@ -96,16 +99,19 @@ protected HmacOneTimePasswordGenerator(final int passwordLength, final String al
9699
switch (passwordLength) {
97100
case 6: {
98101
this.modDivisor = 1_000_000;
102+
this.formatString = "%06d";
99103
break;
100104
}
101105

102106
case 7: {
103107
this.modDivisor = 10_000_000;
108+
this.formatString = "%07d";
104109
break;
105110
}
106111

107112
case 8: {
108113
this.modDivisor = 100_000_000;
114+
this.formatString = "%08d";
109115
break;
110116
}
111117

@@ -160,6 +166,51 @@ public synchronized int generateOneTimePassword(final Key key, final long counte
160166
this.modDivisor;
161167
}
162168

169+
/**
170+
* Generates a one-time password using the given key and counter value and formats it as a string using the system
171+
* default locale.
172+
*
173+
* @param key the key to be used to generate the password
174+
* @param counter the counter value for which to generate the password
175+
*
176+
* @return a string representation of a one-time password
177+
*
178+
* @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
179+
*
180+
* @see Locale#getDefault()
181+
*/
182+
public String generateOneTimePasswordString(final Key key, final long counter) throws InvalidKeyException {
183+
return this.generateOneTimePasswordString(key, counter, Locale.getDefault());
184+
}
185+
186+
/**
187+
* Generates a one-time password using the given key and counter value and formats it as a string using the given
188+
* locale.
189+
*
190+
* @param key the key to be used to generate the password
191+
* @param counter the counter value for which to generate the password
192+
* @param locale the locale to apply during formatting
193+
*
194+
* @return a string representation of a one-time password
195+
*
196+
* @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
197+
*/
198+
public String generateOneTimePasswordString(final Key key, final long counter, final Locale locale) throws InvalidKeyException {
199+
return this.formatOneTimePassword(generateOneTimePassword(key, counter), locale);
200+
}
201+
202+
/**
203+
* Formats a one-time password as a fixed-length string using the given locale.
204+
*
205+
* @param oneTimePassword the one-time password to format as a string
206+
* @param locale the locale to apply during formatting
207+
*
208+
* @return a string representation of the given one-time password
209+
*/
210+
protected String formatOneTimePassword(final int oneTimePassword, final Locale locale) {
211+
return String.format(locale, formatString, oneTimePassword);
212+
}
213+
163214
/**
164215
* Returns the length, in decimal digits, of passwords produced by this generator.
165216
*

src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.security.NoSuchAlgorithmException;
2727
import java.time.Duration;
2828
import java.time.Instant;
29+
import java.util.Locale;
2930

3031
/**
3132
* <p>Generates time-based one-time passwords (TOTP) as specified in
@@ -142,6 +143,38 @@ public int generateOneTimePassword(final Key key, final Instant timestamp) throw
142143
return this.generateOneTimePassword(key, timestamp.toEpochMilli() / this.timeStep.toMillis());
143144
}
144145

146+
/**
147+
* Generates a one-time password using the given key and timestamp and formats it as a string with the system
148+
* default locale.
149+
*
150+
* @param key the key to be used to generate the password
151+
* @param timestamp the timestamp for which to generate the password
152+
*
153+
* @return a string representation of a one-time password
154+
*
155+
* @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
156+
*
157+
* @see Locale#getDefault()
158+
*/
159+
public String generateOneTimePasswordString(final Key key, final Instant timestamp) throws InvalidKeyException {
160+
return this.generateOneTimePasswordString(key, timestamp, Locale.getDefault());
161+
}
162+
163+
/**
164+
* Generates a one-time password using the given key and timestamp and formats it as a string with the given locale.
165+
*
166+
* @param key the key to be used to generate the password
167+
* @param timestamp the timestamp for which to generate the password
168+
* @param locale the locale to apply during formatting
169+
*
170+
* @return a string representation of a one-time password
171+
*
172+
* @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
173+
*/
174+
public String generateOneTimePasswordString(final Key key, final Instant timestamp, final Locale locale) throws InvalidKeyException {
175+
return this.formatOneTimePassword(this.generateOneTimePassword(key, timestamp), locale);
176+
}
177+
145178
/**
146179
* Returns the time step used by this generator.
147180
*

src/main/java/overview.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ <h1>Usage</h1>
4949
<pre>final Instant now = Instant.now();
5050
final Instant later = now.plus(totp.getTimeStep());
5151

52-
System.out.format("Current password: %06d\n", totp.generateOneTimePassword(key, now));
53-
System.out.format("Future password: %06d\n", totp.generateOneTimePassword(key, later));</pre>
52+
System.out.println("Current password: " + totp.generateOneTimePasswordString(key, now));
53+
System.out.println("Future password: " + totp.generateOneTimePasswordString(key, later));</pre>
5454

5555
<h1>License and copyright</h1>
5656

src/test/java/com/eatthepath/otp/ExampleApp.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static void main(final String[] args) throws NoSuchAlgorithmException, In
4444
final Instant now = Instant.now();
4545
final Instant later = now.plus(totp.getTimeStep());
4646

47-
System.out.format("Current password: %06d\n", totp.generateOneTimePassword(key, now));
48-
System.out.format("Future password: %06d\n", totp.generateOneTimePassword(key, later));
47+
System.out.println("Current password: " + totp.generateOneTimePasswordString(key, now));
48+
System.out.println("Future password: " + totp.generateOneTimePasswordString(key, later));
4949
}
5050
}

src/test/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorTest.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.nio.charset.StandardCharsets;
3030
import java.security.Key;
3131
import java.security.NoSuchAlgorithmException;
32+
import java.util.Locale;
3233
import java.util.stream.Stream;
3334

3435
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -73,12 +74,12 @@ void testGetAlgorithm() throws NoSuchAlgorithmException {
7374
* <a href="https://tools.ietf.org/html/rfc4226#appendix-D">RFC&nbsp;4226, Appendix D</a>.
7475
*/
7576
@ParameterizedTest
76-
@MethodSource("hotpTestVectorSource")
77+
@MethodSource("argumentsForTestGenerateOneTimePasswordHotp")
7778
void testGenerateOneTimePassword(final int counter, final int expectedOneTimePassword) throws Exception {
7879
assertEquals(expectedOneTimePassword, this.getDefaultGenerator().generateOneTimePassword(HOTP_KEY, counter));
7980
}
8081

81-
static Stream<Arguments> hotpTestVectorSource() {
82+
private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordHotp() {
8283
return Stream.of(
8384
arguments(0, 755224),
8485
arguments(1, 287082),
@@ -93,6 +94,52 @@ static Stream<Arguments> hotpTestVectorSource() {
9394
);
9495
}
9596

97+
@ParameterizedTest
98+
@MethodSource("argumentsForTestGenerateOneTimePasswordStringHotp")
99+
void testGenerateOneTimePasswordString(final int counter, final String expectedOneTimePassword) throws Exception {
100+
Locale.setDefault(Locale.US);
101+
assertEquals(expectedOneTimePassword, this.getDefaultGenerator().generateOneTimePasswordString(HOTP_KEY, counter));
102+
}
103+
104+
private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringHotp() {
105+
return Stream.of(
106+
arguments(0, "755224"),
107+
arguments(1, "287082"),
108+
arguments(2, "359152"),
109+
arguments(3, "969429"),
110+
arguments(4, "338314"),
111+
arguments(5, "254676"),
112+
arguments(6, "287922"),
113+
arguments(7, "162583"),
114+
arguments(8, "399871"),
115+
arguments(9, "520489")
116+
);
117+
}
118+
119+
@ParameterizedTest
120+
@MethodSource("argumentsForTestGenerateOneTimePasswordStringLocaleHotp")
121+
void testGenerateOneTimePasswordStringLocale(final int counter, final Locale locale, final String expectedOneTimePassword) throws Exception {
122+
Locale.setDefault(Locale.US);
123+
assertEquals(expectedOneTimePassword, this.getDefaultGenerator().generateOneTimePasswordString(HOTP_KEY, counter, locale));
124+
}
125+
126+
private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringLocaleHotp() {
127+
final Locale locale = Locale.forLanguageTag("hi-IN");
128+
129+
return Stream.of(
130+
arguments(0, locale, "७५५२२४"),
131+
arguments(1, locale, "२८७०८२"),
132+
arguments(2, locale, "३५९१५२"),
133+
arguments(3, locale, "९६९४२९"),
134+
arguments(4, locale, "३३८३१४"),
135+
arguments(5, locale, "२५४६७६"),
136+
arguments(6, locale, "२८७९२२"),
137+
arguments(7, locale, "१६२५८३"),
138+
arguments(8, locale, "३९९८७१"),
139+
arguments(9, locale, "५२०४८९")
140+
);
141+
}
142+
96143
protected HmacOneTimePasswordGenerator getDefaultGenerator() throws NoSuchAlgorithmException {
97144
return new HmacOneTimePasswordGenerator();
98145
}

src/test/java/com/eatthepath/otp/TimeBasedOneTimePasswordGeneratorTest.java

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.security.NoSuchAlgorithmException;
3232
import java.time.Duration;
3333
import java.time.Instant;
34+
import java.util.Locale;
3435
import java.util.stream.Stream;
3536

3637
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -74,8 +75,8 @@ void testGetTimeStep() throws NoSuchAlgorithmException {
7475
* different keys are used for each of the various HMAC algorithms.
7576
*/
7677
@ParameterizedTest
77-
@MethodSource("totpTestVectorSource")
78-
void testGenerateOneTimePassword(final String algorithm, final Key key, final long epochSeconds, final int expectedOneTimePassword) throws Exception {
78+
@MethodSource("argumentsForTestGenerateOneTimePasswordTotp")
79+
void testGenerateOneTimePasswordTotp(final String algorithm, final Key key, final long epochSeconds, final int expectedOneTimePassword) throws Exception {
7980

8081
final TimeBasedOneTimePasswordGenerator totp =
8182
new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 8, algorithm);
@@ -85,7 +86,7 @@ void testGenerateOneTimePassword(final String algorithm, final Key key, final lo
8586
assertEquals(expectedOneTimePassword, totp.generateOneTimePassword(key, timestamp));
8687
}
8788

88-
static Stream<Arguments> totpTestVectorSource() {
89+
private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordTotp() {
8990
return Stream.of(
9091
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 59L, 94287082),
9192
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 1111111109L, 7081804),
@@ -107,4 +108,76 @@ static Stream<Arguments> totpTestVectorSource() {
107108
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 20000000000L, 47863826)
108109
);
109110
}
111+
112+
@ParameterizedTest
113+
@MethodSource("argumentsForTestGenerateOneTimePasswordStringTotp")
114+
void testGenerateOneTimePasswordStringTotp(final String algorithm, final Key key, final long epochSeconds, final String expectedOneTimePassword) throws Exception {
115+
116+
final TimeBasedOneTimePasswordGenerator totp =
117+
new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 8, algorithm);
118+
119+
final Instant timestamp = Instant.ofEpochSecond(epochSeconds);
120+
121+
assertEquals(expectedOneTimePassword, totp.generateOneTimePasswordString(key, timestamp));
122+
}
123+
124+
private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringTotp() {
125+
return Stream.of(
126+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 59L, "94287082"),
127+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 1111111109L, "07081804"),
128+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 1111111111L, "14050471"),
129+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 1234567890L, "89005924"),
130+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 2000000000L, "69279037"),
131+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 20000000000L, "65353130"),
132+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 59L, "46119246"),
133+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 1111111109L, "68084774"),
134+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 1111111111L, "67062674"),
135+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 1234567890L, "91819424"),
136+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 2000000000L, "90698825"),
137+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 20000000000L, "77737706"),
138+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 59L, "90693936"),
139+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 1111111109L, "25091201"),
140+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 1111111111L, "99943326"),
141+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 1234567890L, "93441116"),
142+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 2000000000L, "38618901"),
143+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 20000000000L, "47863826")
144+
);
145+
}
146+
147+
@ParameterizedTest
148+
@MethodSource("argumentsForTestGenerateOneTimePasswordStringLocaleTotp")
149+
void testGenerateOneTimePasswordStringLocaleTotp(final String algorithm, final Key key, final long epochSeconds, final Locale locale, final String expectedOneTimePassword) throws Exception {
150+
151+
final TimeBasedOneTimePasswordGenerator totp =
152+
new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(30), 8, algorithm);
153+
154+
final Instant timestamp = Instant.ofEpochSecond(epochSeconds);
155+
156+
assertEquals(expectedOneTimePassword, totp.generateOneTimePasswordString(key, timestamp, locale));
157+
}
158+
159+
private static Stream<Arguments> argumentsForTestGenerateOneTimePasswordStringLocaleTotp() {
160+
final Locale locale = Locale.forLanguageTag("hi-IN");
161+
162+
return Stream.of(
163+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 59L, locale, "९४२८७०८२"),
164+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 1111111109L, locale, "०७०८१८०४"),
165+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 1111111111L, locale, "१४०५०४७१"),
166+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 1234567890L, locale, "८९००५९२४"),
167+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 2000000000L, locale, "६९२७९०३७"),
168+
arguments(HMAC_SHA1_ALGORITHM, HMAC_SHA1_KEY, 20000000000L, locale, "६५३५३१३०"),
169+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 59L, locale, "४६११९२४६"),
170+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 1111111109L, locale, "६८०८४७७४"),
171+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 1111111111L, locale, "६७०६२६७४"),
172+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 1234567890L, locale, "९१८१९४२४"),
173+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 2000000000L, locale, "९०६९८८२५"),
174+
arguments(HMAC_SHA256_ALGORITHM, HMAC_SHA256_KEY, 20000000000L, locale, "७७७३७७०६"),
175+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 59L, locale, "९०६९३९३६"),
176+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 1111111109L, locale, "२५०९१२०१"),
177+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 1111111111L, locale, "९९९४३३२६"),
178+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 1234567890L, locale, "९३४४१११६"),
179+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 2000000000L, locale, "३८६१८९०१"),
180+
arguments(HMAC_SHA512_ALGORITHM, HMAC_SHA512_KEY, 20000000000L, locale, "४७८६३८२६")
181+
);
182+
}
110183
}

0 commit comments

Comments
 (0)