DAREK

Google Authenticator的原理与实现

写在前面

Google Authenticator是谷歌推出的一款动态口令工具,为了提供固定密码之外的二步验证,减小因密码泄露导致的安全问题。

作为1Password的重度用户,我使用1Password内置的一次性密码功能生成动态密码,1Password、Authy、Google Authenticator等一次性密码生成工具,其实原理都是 RFC6238 Time-based One-time Password Algorithm

TOTP的好处在于验证过程中不会泄露私钥,部分银行网银、游戏安全登录等动态密码都是基于此的。

原理

一句话解释就是

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

因素

步骤

  1. 一次性密码通常为文本或二维码,内容为base32后的密钥。所以要先将密钥base32解密
  2. 用密钥对当天时间step进行HMAC-SHA1加密
  3. 将最后一个字节的低4位二进制作为索引,索引范围为0-15
  4. 计算索引指向的连续4字节空间生成int整型数据
  5. 对获取到的整型数据进行模运算(取后六位)
  6. 对结果进行补全(长度不够6位,在首位补零)

Java实现

再怎么说都不如直接看代码来的实际

如下,或见Github TOTP4GoogleAuthenticator

 

public class TOTP {

    private Mac mac;

    public TOTP() {
        try {
            //HMAC: H(K XOR opad, H(K XOR ipad, text))
            mac = Mac.getInstance("HmacSHA1");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

    }

    /**
     * @param key        base32加密后私钥
     * @param systemTime 当前时间
     * @param invertal   时间差
     * @return GoogleAuthenticator Number Code
     * @throws Exception
     */
    public String getDynamicCode(String key, long systemTime, int invertal) throws Exception {
        //用密钥和当前step计算
        byte[] hash = getHmacSHA1(key, (systemTime - invertal) / 30000);
        //将最后一个字节的低4位二进制作为索引,索引范围为0-15
        int offset = hash[19] & 0xf;
        //然后计算索引指向的连续4字节空间生成int整型数据。
        int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
        //对获取到的整型数据进行模运算(取后六位)
        int otp = binary % 1000000;
        //对结果进行补全(长度不够6位,在首位补零)
        return addZeros(Integer.toString(otp));
    }

    /**
     * 计算HMAC-SHA-1
     *
     * @param secret 秘钥
     * @param msg    需要加密内容
     * @return
     * @throws Exception
     */
    private byte[] getHmacSHA1(String secret, long msg) throws Exception {
        SecretKey secretKey = new SecretKeySpec(Base32String.decode(secret), "RAW");
        mac.reset();
        mac.init(secretKey);
        byte[] value = ByteBuffer.allocate(8).putLong(msg).array();
        return mac.doFinal(value);
    }

    /**
     * 以"0"补足6位
     *
     * @param s
     * @return
     */
    private String addZeros(String s) {
        if (s.length() < 6) {
            s = "0" + s;
            return addZeros(s);
        }
        return s;
    }

    public static void main(String[] args) {
        TOTP totp = new TOTP();
        try {
            String dynamicCode = totp.getDynamicCode("GEZDGNBTGI2DINBU", System.currentTimeMillis(), 0);
            System.out.println(dynamicCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

最后

其实整个算法并不复杂,只要满足Google定义的算法规范即可。希望更多的网站能够启用一次性密码验证,真心不喜欢动不动就自己搞一套,结果手机上一堆QQ安全中心、阿里钱盾的软件。

⬅️ Go back