Google Authenticator Counter Based OTP

0

Hi I am using Counter based OTP with HOTPAlgorithm which is as below.

When I try to generate code using google authenticator App by entering same secret key as of my server,its produce different code than my actual server side code which is generate by below algorithm.Please help.

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.http.client.utils.URIBuilder;

public class HOTPAlgorithm {
    private HOTPAlgorithm() {
    }

    // These are used to calculate the check-sum digits.
    // 0 1 2 3 4 5 6 7 8 9
    private static final int[] doubleDigits = { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 };

    /**
     * Calculates the checksum using the credit card algorithm. This algorithm
     * has the advantage that it detects any single mistyped digit and any
     * single transposition of adjacent digits.
     *
     * @param num
     *            the number to calculate the checksum for
     * @param digits
     *            number of significant places in the number
     *
     * @return the checksum of num
     */
    public static int calcChecksum(long num, int digits) {
        boolean doubleDigit = true;
        int total = 0;
        while (0 < digits--) {
            int digit = (int) (num % 10);
            num /= 10;
            if (doubleDigit) {
                digit = doubleDigits[digit];
            }
            total += digit;
            doubleDigit = !doubleDigit;
        }
        int result = total % 10;
        if (result > 0) {
            result = 10 - result;
        }
        return result;
    }

    /**
     * This method uses the JCE to provide the HMAC-SHA-1 algorithm. HMAC
     * computes a Hashed Message Authentication Code and in this case SHA1 is
     * the hash algorithm used.
     *
     * @param keyBytes
     *            the bytes to use for the HMAC-SHA-1 key
     * @param text
     *            the message or text to be authenticated.
     *
     * @throws NoSuchAlgorithmException
     *             if no provider makes either HmacSHA1 or HMAC-SHA-1 digest
     *             algorithms available.
     * @throws InvalidKeyException
     *             The secret provided was not a valid HMAC-SHA-1 key.
     *
     */
    public static byte[] hmac_sha1(byte[] keyBytes, byte[] text) throws NoSuchAlgorithmException, InvalidKeyException {
        // try {
        Mac hmacSha1;
        try {
            hmacSha1 = Mac.getInstance("HmacSHA1");
        } catch (NoSuchAlgorithmException nsae) {
            hmacSha1 = Mac.getInstance("HMAC-SHA-1");
        }
        SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
        hmacSha1.init(macKey);
        return hmacSha1.doFinal(text);
        // } catch (GeneralSecurityException gse) {
        // throw new UndeclaredThrowableException(gse);
        // }
    }

    private static final int[] DIGITS_POWER // 0 1 2 3 4 5 6 7 8
    = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };

    /**
     * This method generates an OTP value for the given set of parameters.
     *
     * @param secret
     *            the shared secret
     * @param movingFactor
     *            the counter, time, or other value that changes on a per use
     *            basis.
     * @param codeDigits
     *            the number of digits in the OTP, not including the checksum,
     *            if any.
     * @param addChecksum
     *            a flag that indicates if a checksum digit should be appended
     *            to the OTP.
     * @param truncationOffset
     *            the offset into the MAC result to begin truncation. If this
     *            value is out of the range of 0 ... 15, then dynamic truncation
     *            will be used. Dynamic truncation is when the last 4 bits of
     *            the last byte of the MAC are used to determine the start
     *            offset.
     * @throws NoSuchAlgorithmException
     *             if no provider makes either HmacSHA1 or HMAC-SHA-1 digest
     *             algorithms available.
     * @throws InvalidKeyException
     *             The secret provided was not a valid HMAC-SHA-1 key.
     *
     * @return A numeric String in base 10 that includes
     */
    static public String generateOTP(byte[] secret, long movingFactor, int codeDigits, boolean addChecksum,
            int truncationOffset) throws NoSuchAlgorithmException, InvalidKeyException {
        // put movingFactor value into text byte array
        /*
         * Base32 base32 = new Base32(); secret=base32.decode(secret);
         */
        String result = null;
        int digits = addChecksum ? (codeDigits + 1) : codeDigits;
        byte[] text = new byte[8];
        for (int i = text.length - 1; i >= 0; i--) {
            text[i] = (byte) (movingFactor & 0xff);
            movingFactor >>= 8;
        }

        // compute hmac hash

        byte[] hash = hmac_sha1(secret, text);
        //System.out.println("hash" + new String(hash));
        // put selected bytes into result int
        int offset = hash[hash.length - 1] & 0xf;
        /*if ((0 <= truncationOffset) && (truncationOffset < (hash.length - 4))) {
            offset = truncationOffset;
        }*/

        //offset = hash[hash.length - 1] & 0xF;
        //System.out.println("offset"+offset);

        int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16)
                | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);

        int otp = (int) (binary % Math.pow(10, codeDigits));
        if (addChecksum) {
            otp = (otp * 10) + calcChecksum(otp, codeDigits);
        }
        result = Integer.toString(otp);
        while (result.length() < digits) {
            result = "0" + result;
        }
        return result;
    }

    public static int calculateCode(byte[] key, long tm) {
        // Allocating an array of bytes to represent the specified instant
        // of time.
        byte[] data = new byte[8];
        long value = tm;

        // Converting the instant of time from the long representation to a
        // big-endian array of bytes (RFC4226, 5.2. Description).
        /*
         * for (int i = 8; i-- > 0; value >>>= 8) { data[i] = (byte) value; }
         */

        /*
         * for (int i = data.length - 1; i >= 0; i--) { data[i] = (byte) (value
         * & 0xff); value >>= 8; }
         */

        for (int i = 8; i-- > 0; value >>>= 8) {
            data[i] = (byte) value;
        }

        // Building the secret key specification for the HmacSHA1 algorithm.
        SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");

        try {
            // Getting an HmacSHA1 algorithm implementation from the JCE.
            Mac mac = Mac.getInstance("HmacSHA1");

            // Initializing the MAC algorithm.
            mac.init(signKey);

            // Processing the instant of time and getting the encrypted data.
            byte[] hash = mac.doFinal(data);
            System.out.println("hash1" + new String(hash));
            // Building the validation code performing dynamic truncation
            // (RFC4226, 5.3. Generating an HOTP value)
            int offset = hash[hash.length - 1] & 0xF;
            // offset=0;
            // We are using a long because Java hasn't got an unsigned integer
            // type
            // and we need 32 unsigned bits).
            int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16)
                    | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);

            long truncatedHash = 0;

            for (int i = 0; i < 4; ++i) {
                truncatedHash <<= 8;

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

            // Clean bits higher than the 32nd (inclusive) and calculate the
            // module with the maximum validation code value.
            truncatedHash &= 0x7FFFFFFF;
            truncatedHash %= (int) Math.pow(10, 6);
            int otp = (int) (binary % Math.pow(10, 6));

            // Returning the validation code to the caller.
            return (int) truncatedHash;
        } catch (Exception ex) {
            // Logging the exception.

            return 0;
            // We're not disclosing internal error details to our clients.

        }

    }
     private static final String TOTP_URI_FORMAT =
                "https://chart.eCWapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=%s";

    public static String internalURLEncode(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 encoding is not supported by URLEncoder.", e);
        }
    }

    public static String getOtpAuthURL(String issuer, String accountName, String credentials) {

        return String.format(TOTP_URI_FORMAT, internalURLEncode(getOtpAuthTotpURL(issuer, accountName, credentials)));
    }

    public static String getOtpAuthTotpURL(String issuer, String accountName, String credentials) {

        URIBuilder uri = new URIBuilder().setScheme("otpauth").setHost("totp")
                .setPath("/" + formatLabel(issuer, accountName)).setParameter("secret", credentials);

        if (issuer != null) {
            if (issuer.contains(":")) {
                throw new IllegalArgumentException("Issuer cannot contain the \':\' character.");
            }

            uri.setParameter("issuer", issuer);
        }

        /*
         * The following parameters aren't needed since they are all defaults.
         * We can exclude them to make the URI shorter.
         */
        // uri.setParameter("algorithm", "SHA1");
        // uri.setParameter("digits", "6");
        // uri.setParameter("period", "30");

        return uri.toString();

    }

    private static String formatLabel(String issuer, String accountName) {
        if (accountName == null || accountName.trim().length() == 0) {
            throw new IllegalArgumentException("Account name must not be empty.");
        }

        StringBuilder sb = new StringBuilder();
        if (issuer != null) {
            if (issuer.contains(":")) {
                throw new IllegalArgumentException("Issuer cannot contain the \':\' character.");
            }

            sb.append(issuer);
            sb.append(":");
        }

        sb.append(accountName);

        return sb.toString();
    }

}
security
google-authenticator
asked on Stack Overflow Aug 9, 2016 by Jekin Kalariya • edited Aug 11, 2016 by Jekin Kalariya

2 Answers

1

this did not work for me - I've tested several variants and the only working for me is:

public static void main(String[] args) throws InvalidKeyException, NoSuchAlgorithmException, DecodingException {
    // Seed
    // byte[] secret = { 'H', 'e', 'l', 'l', 'o', '!', (byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF }; // as per information by https://github.com/google/google-authenticator/wiki/Key-Uri-Format 
    String secrethashed = "JBSWY3DPEHPK3PXP"; // enter this code to your Google Authenticator Application and you should get the same results
    byte[] secret = Base32String.decode(secrethashed);
    // byte[] secretBytes = secret.getBytes();

    int counter;
    for (counter = 0; counter < 10; counter++) {
        String strGeneratedToken = OneTimePasswordAlgorithm.generateOTP(secret, counter, 6, false, 16);
        System.out.println(strGeneratedToken);
    }
}

while I'm using the official Base32String Class provided by Google and used in their Google Authenticator Application Github Project

I've created a variant of that code (working - I've been comparing results) to https://github.com/n0l0cale/hotp/tree/GoogleAuthenticatorVariant

Cheers S.

answered on Stack Overflow Jun 19, 2018 by Sebastian Vivten
0

I got this resolved by changing initial counter value which should be initially 1 and also at time of validation you need to pass secret key with base32 decode format. Algorithm will remain same as above

String secret = "ABCDEABCDE";   
Base32 base32 = new Base32();
byte barray[]=base32 .decode(secret);
HOTPAlgorithm.generateOTP(barray, 1l, 6, false, 0);//1l is initial moving factor which is
answered on Stack Overflow Aug 11, 2016 by Jekin Kalariya

User contributions licensed under CC BY-SA 3.0