基于Google身份验证器计数器的OTP

时间:2016-08-09 07:03:17

标签: security google-authenticator

您好我使用基于计数器的OTP和HOTP算法,如下所示。

当我尝试使用谷歌身份验证器应用程序通过输入与我的服务器相同的密钥来生成代码时,它产生的代码与我通过以下算法生成的实际服务器端代码不同。请帮助。

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();
    }

}

2 个答案:

答案 0 :(得分:1)

这对我不起作用-我已经测试了几种变体,唯一对我有用的是:

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);
    }
}

当我使用Google提供的官方Base32String类并在其Google Authenticator应用程序Github项目中使用

我已经创建了该代码的变体(正在工作-我一直在比较结果)到https://github.com/n0l0cale/hotp/tree/GoogleAuthenticatorVariant

干杯S。

答案 1 :(得分:0)

我通过更改初始计数器值来解决此问题,初始计数器值应该是最初的1,并且在验证时您需要使用base32解码格式传递密钥。算法将保持与上述相同

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