All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ackbox.totp.google.GoogleAuthenticator.kt Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
package com.ackbox.totp.google

import com.ackbox.totp.TOTPAuthenticator
import com.ackbox.totp.TOTPException
import com.ackbox.totp.TOTPSecretKey
import com.ackbox.totp.ReseedingSecureRandom
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.time.Instant
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.and

/**
 * This class implements the functionality described in RFC 6238 (TOTP: Time
 * based one-time password algorithm) and has been tested again Google's
 * implementation of such algorithm in its Google Authenticator application.
 *
 * This class lets users create a new 16-bit base32-encoded secret key with
 * the validation code calculated at `time = 0` (the UNIX epoch) and the
 * URL of a Google-provided QR barcode to let an user load the generated
 * information into Google Authenticator.
 *
 * Java Server side class for Google Authenticator's TOTP generator was inspired by an author's blog post.
 *
 * @see [Blog Post](http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html)
 * @see [Google Authenticator](http://code.google.com/p/google-authenticator)
 * @see [HOTP Time Based](http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt)
 */
class GoogleAuthenticator(private val config: GoogleAuthenticatorConfig = GoogleAuthenticatorConfig()) : TOTPAuthenticator {

    private val secureRandom = ReseedingSecureRandom()

    override fun createCredentials(): GoogleTOTPCredentials {
        // Allocating a buffer sufficiently large to hold the bytes required by the secret key and the scratch codes.
        val buffer = ByteArray(SECRET_BITS / 8 + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE)
        secureRandom.nextBytes(buffer)

        // Extracting the bytes making up the secret key.
        val key = Arrays.copyOf(buffer, SECRET_BITS / 8)
        val validationCode = calculateValidationCode(key)
        val scratchCodes = calculateScratchCodes(buffer)

        return GoogleTOTPCredentials(TOTPSecretKey(key), validationCode, scratchCodes)
    }

    override fun authorize(secretKey: TOTPSecretKey, totp: Int, time: Instant): Boolean {
        // Checking if the verification code is between the legal bounds.
        if (totp <= 0 || totp >= config.keyModulus) {
            return false
        }
        return checkCode(secretKey.value, totp, time, config.windowSize)
    }

    override fun createOneTimePassword(secretKey: TOTPSecretKey, time: Instant): Int {
        return calculateCode(secretKey.value, getTimeWindowFromTime(time))
    }

    /**
     * Calculates the verification code of the provided key at the specified
     * instant of time using the algorithm specified in RFC 6238.
     *
     * @param key the secret key in binary format.
     * @param timestamp the instant of time.
     * @return the validation code for the provided key at the specified instant of time.
     */
    private fun calculateCode(key: ByteArray, timestamp: Long): Int {
        // Converting the instant of time from the long representation to a  big-endian array of bytes (RFC4226, 5.2. Description).
        val bigEndianTimestamp = ByteArray(8)
        var value = timestamp
        var byte = 8
        while (byte-- > 0) {
            bigEndianTimestamp[byte] = value.toByte()
            value = value ushr 8
        }

        // Building the secret key specification for the HmacSHA1 algorithm.
        val signKey = SecretKeySpec(key, HMAC_HASH_FUNCTION)

        try {
            // Getting an HmacSHA1 algorithm implementation from the JCE.
            val mac = Mac.getInstance(HMAC_HASH_FUNCTION)
            mac.init(signKey)

            // Processing the instant of time and getting the encrypted data.
            val hash = mac.doFinal(bigEndianTimestamp)

            // Building the validation code performing dynamic truncation (RFC4226, 5.3. Generating an HOTP value)
            val offset = hash[hash.size - 1] and 0xF

            // We are using a long because Java hasn't got an unsigned integer type and we need 32 unsigned bits).
            var truncatedHash: Long = 0

            for (i in 0..3) {
                truncatedHash = truncatedHash shl 8

                // Java bytes are signed but we need an unsigned integer: cleaning off all but the LSB.
                truncatedHash = truncatedHash or (hash[offset + i].toInt() and 0xFF).toLong()
            }

            // Clean bits higher than the 32nd (inclusive) and calculate the module with the maximum validation code value.
            truncatedHash = truncatedHash and 0x7FFFFFFF
            truncatedHash %= config.keyModulus

            return truncatedHash.toInt()
        } catch (e: NoSuchAlgorithmException) {
            throw TOTPException("The operation cannot be performed now.", e)
        } catch (e: InvalidKeyException) {
            throw TOTPException("The operation cannot be performed now.", e)
        }
    }

    /**
     * This method implements the algorithm specified in RFC 6238 to check if a
     * validation code is valid in a given instant of time for the given secret key.
     *
     * @param key encoded secret key.
     * @param code the code to validate.
     * @param timestamp the instant of time to use during the validation process.
     * @param window the window size to use during the validation process.
     * @return `true` if the validation code is valid, `false` otherwise.
     */
    private fun checkCode(key: ByteArray, code: Int, timestamp: Instant, window: Int): Boolean {
        // convert unix time into a 30 second "window" as specified by the
        // TOTP specification. Using Google's default interval of 30 seconds.
        val timeWindow = getTimeWindowFromTime(timestamp)

        // Calculating the verification code of the given key in each of the
        // time intervals and returning true if the provided code is equal to
        // one of them.
        val start =  -((window - 1) / 2)
        val end = window / 2
        for (i in start..end) {
            // Calculating the verification code for the current time interval.
            val hash = calculateCode(key, timeWindow + i)
            // Checking if the provided code is equal to the calculated one.
            if (hash == code) {
                return true
            }
        }
        return false
    }

    /**
     * This method calculates the validation code at time 0.
     *
     * @param key The secret key to use.
     * @return the validation code at time 0.
     */
    private fun calculateValidationCode(key: ByteArray): Int = calculateCode(key, 0)

    private fun getTimeWindowFromTime(time: Instant) = time.toEpochMilli() / config.timeStepSize.toMillis()

    private fun calculateScratchCodes(buffer: ByteArray): List {
        val scratchCodes = ArrayList()
        while (scratchCodes.size < SCRATCH_CODES) {
            val scratchCodeBuffer = Arrays.copyOfRange(
                buffer,
                SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size,
                SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size + BYTES_PER_SCRATCH_CODE)

            val scratchCode = calculateScratchCode(scratchCodeBuffer)
            if (scratchCode != SCRATCH_CODE_INVALID) {
                scratchCodes.add(scratchCode)
            } else {
                scratchCodes.add(generateScratchCode())
            }
        }
        return scratchCodes
    }

    /**
     * This method creates a new random byte buffer from which a new scratch
     * code is generated. This function is invoked if a scratch code generated
     * from the main buffer is invalid because it does not satisfy the scratch
     * code restrictions.
     *
     * @return A valid scratch code.
     */
    private fun generateScratchCode(): Int {
        while (true) {
            val scratchCodeBuffer = ByteArray(BYTES_PER_SCRATCH_CODE)
            secureRandom.nextBytes(scratchCodeBuffer)
            val scratchCode = calculateScratchCode(scratchCodeBuffer)
            if (scratchCode != SCRATCH_CODE_INVALID) {
                return scratchCode
            }
        }
    }

    /**
     * This method calculates a scratch code from a random byte buffer of
     * suitable size `#BYTES_PER_SCRATCH_CODE`.
     *
     * @param scratchCodeBuffer a random byte buffer whose minimum size is `#BYTES_PER_SCRATCH_CODE`.
     * @return the scratch code.
     */
    private fun calculateScratchCode(scratchCodeBuffer: ByteArray): Int {
        if (scratchCodeBuffer.size < BYTES_PER_SCRATCH_CODE) {
            throw IllegalArgumentException("The provided random byte buffer is too small ${scratchCodeBuffer.size}.")
        }

        var scratchCode = 0
        for (i in 0..BYTES_PER_SCRATCH_CODE - 1) {
            scratchCode = (scratchCode shl 8) + (scratchCodeBuffer[i] and 0xff.toByte())
        }

        scratchCode = (scratchCode and 0x7FFFFFFF) % SCRATCH_CODE_MODULUS

        // Accept the scratch code only if it has exactly SCRATCH_CODE_LENGTH digits.
        if (validateScratchCode(scratchCode)) {
            return scratchCode
        }
        return SCRATCH_CODE_INVALID
    }

    private fun validateScratchCode(scratchCode: Int) = scratchCode >= SCRATCH_CODE_MODULUS / 10

    companion object {

        /**
         * The number of bits of a secret key in binary form. Since the Base32
         * encoding with 8 bit characters introduces an 160% overhead, we just need
         * 80 bits (10 bytes) to generate a 16 bytes Base32-encoded secret key.
         */
        private val SECRET_BITS = 80

        /**
         * Number of scratch codes to generate during the key generation.
         * We are using Google's default of providing 5 scratch codes.
         */
        private val SCRATCH_CODES = 5

        /**
         * Number of digits of a scratch code represented as a decimal integer.
         */
        private val SCRATCH_CODE_LENGTH = 8

        /**
         * Modulus used to truncate the scratch code.
         */
        private val SCRATCH_CODE_MODULUS = Math.pow(10.0, SCRATCH_CODE_LENGTH.toDouble()).toInt()

        /**
         * Magic number representing an invalid scratch code.
         */
        private val SCRATCH_CODE_INVALID = -1

        /**
         * Length in bytes of each scratch code. We're using Google's default of
         * using 4 bytes per scratch code.
         */
        private val BYTES_PER_SCRATCH_CODE = 4

        private val HMAC_HASH_FUNCTION = "HmacSHA1"
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy