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 */