diff --git a/src/benchmark/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorBenchmark.java b/src/benchmark/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorBenchmark.java
index 1d46ed5..676a7e9 100644
--- a/src/benchmark/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorBenchmark.java
+++ b/src/benchmark/java/com/eatthepath/otp/HmacOneTimePasswordGeneratorBenchmark.java
@@ -10,7 +10,7 @@
import java.security.Key;
import java.security.NoSuchAlgorithmException;
-@State(Scope.Thread)
+@State(Scope.Benchmark)
public class HmacOneTimePasswordGeneratorBenchmark {
private HmacOneTimePasswordGenerator hotp;
diff --git a/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java b/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java
index 0cf32a0..c9db579 100644
--- a/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java
+++ b/src/main/java/com/eatthepath/otp/HmacOneTimePasswordGenerator.java
@@ -20,30 +20,27 @@
package com.eatthepath.otp;
-import java.nio.ByteBuffer;
+import javax.crypto.Mac;
+import javax.crypto.ShortBufferException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
-import javax.crypto.Mac;
-
/**
*
Generates HMAC-based one-time passwords (HOTP) as specified in
* RFC 4226.
*
- * {@code HmacOneTimePasswordGenerator} instances are thread-safe and may be shared and re-used across multiple
- * threads.
+ * {@code HmacOneTimePasswordGenerator} instances are thread-safe and may be shared between threads.
*
* @author Jon Chambers
*/
public class HmacOneTimePasswordGenerator {
- private final String algorithm;
+ private final Mac mac;
private final int passwordLength;
+ private final byte[] buffer;
private final int modDivisor;
- private final ThreadLocal macThreadLocal;
-
/**
* The default length, in decimal digits, for one-time passwords.
*/
@@ -94,6 +91,8 @@ public HmacOneTimePasswordGenerator(final int passwordLength) throws NoSuchAlgor
* @throws NoSuchAlgorithmException if the given algorithm is not supported by the underlying JRE
*/
protected HmacOneTimePasswordGenerator(final int passwordLength, final String algorithm) throws NoSuchAlgorithmException {
+ this.mac = Mac.getInstance(algorithm);
+
switch (passwordLength) {
case 6: {
this.modDivisor = 1_000_000;
@@ -117,19 +116,8 @@ protected HmacOneTimePasswordGenerator(final int passwordLength, final String al
this.passwordLength = passwordLength;
- // Our purpose here is just to throw an exception immediately if the algorithm is bogus.
- Mac.getInstance(algorithm);
- this.algorithm = algorithm;
-
- this.macThreadLocal = ThreadLocal.withInitial(() -> {
- try {
- return Mac.getInstance(algorithm);
- } catch (final NoSuchAlgorithmException e) {
- // This should never happen; we just checked to make sure we could instantiate a MAC with this
- // algorithm, and if we made it this far, we know it worked.
- throw new RuntimeException(e);
- }
- });
+ // We need at least 8 bytes to store the 64-bit counter value
+ this.buffer = new byte[Math.max(8, this.mac.getMacLength())];
}
/**
@@ -143,25 +131,35 @@ protected HmacOneTimePasswordGenerator(final int passwordLength, final String al
*
* @throws InvalidKeyException if the given key is inappropriate for initializing the {@link Mac} for this generator
*/
- public int generateOneTimePassword(final Key key, final long counter) throws InvalidKeyException {
- final Mac mac = this.macThreadLocal.get();
- mac.init(key);
-
- final ByteBuffer buffer = ByteBuffer.allocate(8);
- buffer.putLong(0, counter);
-
- final byte[] hmac = mac.doFinal(buffer.array());
- final int offset = hmac[hmac.length - 1] & 0x0f;
-
- for (int i = 0; i < 4; i++) {
- // Note that we're re-using the first four bytes of the buffer here; we just ignore the latter four from
- // here on out.
- buffer.put(i, hmac[i + offset]);
+ public synchronized int generateOneTimePassword(final Key key, final long counter) throws InvalidKeyException {
+ this.mac.init(key);
+
+ this.buffer[0] = (byte) ((counter & 0xff00000000000000L) >>> 56);
+ this.buffer[1] = (byte) ((counter & 0x00ff000000000000L) >>> 48);
+ this.buffer[2] = (byte) ((counter & 0x0000ff0000000000L) >>> 40);
+ this.buffer[3] = (byte) ((counter & 0x000000ff00000000L) >>> 32);
+ this.buffer[4] = (byte) ((counter & 0x00000000ff000000L) >>> 24);
+ this.buffer[5] = (byte) ((counter & 0x0000000000ff0000L) >>> 16);
+ this.buffer[6] = (byte) ((counter & 0x000000000000ff00L) >>> 8);
+ this.buffer[7] = (byte) (counter & 0x00000000000000ffL);
+
+ this.mac.update(this.buffer, 0, 8);
+
+ try {
+ this.mac.doFinal(this.buffer, 0);
+ } catch (final ShortBufferException e) {
+ // We allocated the buffer to (at least) match the size of the MAC length at construction time, so this
+ // should never happen.
+ throw new RuntimeException(e);
}
- final int hotp = buffer.getInt(0) & 0x7fffffff;
+ final int offset = this.buffer[this.mac.getMacLength() - 1] & 0x0f;
- return hotp % this.modDivisor;
+ return ((this.buffer[offset] & 0x7f) << 24 |
+ (this.buffer[offset + 1] & 0xff) << 16 |
+ (this.buffer[offset + 2] & 0xff) << 8 |
+ (this.buffer[offset + 3] & 0xff)) %
+ this.modDivisor;
}
/**
@@ -179,6 +177,6 @@ public int getPasswordLength() {
* @return the name of the HMAC algorithm used by this generator
*/
public String getAlgorithm() {
- return this.algorithm;
+ return this.mac.getAlgorithm();
}
}
diff --git a/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java b/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java
index 57ed5d0..21df927 100644
--- a/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java
+++ b/src/main/java/com/eatthepath/otp/TimeBasedOneTimePasswordGenerator.java
@@ -31,8 +31,7 @@
* Generates time-based one-time passwords (TOTP) as specified in
* RFC 6238.
*
- * {@code TimeBasedOneTimePasswordGenerator} instances are thread-safe and may be shared and re-used across multiple
- * threads.
+ * {@code TimeBasedOneTimePasswordGenerator} instances are thread-safe and may be shared between threads.
*
* @author Jon Chambers
*/