Java利用TOTP算法动态生成一次性密码

一、HOTP

  HOTP 算法,全称是“An HMAC-Based One-Time Password Algorithm”,是一种基于事件计数的一次性密码生成算法,详细的算法介绍可以查看 RFC 4226。其实算法本身非常简单,算法本身可以用两条简短的表达式描述:

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

PWD(K,C,digit) = HOTP(K,C) mod 10^Digit



二、TOTP

  TOTP 算法,全称是 TOTP: Time-Based One-Time Password Algorithm,其基于 HOTP 算法实现,核心是将移动因子从 HOTP 中的事件计数改为时间差。完整的 TOTP 算法的说明可以查看 RFC 6238,其公式描述也非常简单:

TOTP = HOTP(K, T) // T is an integer

and represents the number of time steps between the initial counter

time T0 and the current Unix time

More specifically, T = (Current Unix time - T0) / X, where the

default floor function is used in the computation.

三、TOTP具体Java实现

  子服务端:启用不含verifyTOTP*()验证方法的TOTP,使用子服务端的账户和密码加密,向验证服务端发送动态口令。
  验证服务端:验证子服务端的口令,根据子服务端标识信息,使用其账户和密码加密得到口令,比对口令是否一致即可。
  前提:因为使用时间作为动态因子加密口令,子服务端的时间应和验证服务端的时间一致,比如使用同一个授时服务器授时。
  推荐使用柔性口令验证,使用柔性验证时请设置时间回溯参数,避免因口令在网络中传输消耗时间,或者服务端时间误差导致口令失效。

/**
 * <p>ClassName: TOTP</p>
 * <p>Description: TOTP = HOTP(K, T) // T is an integer
 * and represents the number of time steps between the initial counter
 * time T0 and the current Unix time
 * <p>
 * More specifically, T = (Current Unix time - T0) / X, where the
 * default floor function is used in the computation.</p>
 *
 */
public class TOTP {
 
    public static void main(String[] args) {
        try {
 
            for (int j = 0; j < 10; j++) {
                String totp = generateMyTOTP("account01", "12345");
                System.out.println(String.format("加密后: %s", totp));
                Thread.sleep(1000);
            }
 
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 共享密钥
     */
    private static final String SECRET_KEY = "ga35sdia43dhqj6k3f0la";
 
    /**
     * 时间步长 单位:毫秒 作为口令变化的时间周期
     */
    private static final long STEP = 30000;
 
    /**
     * 转码位数 [1-8]
     */
    private static final int CODE_DIGITS = 8;
 
    /**
     * 初始化时间
     */
    private static final long INITIAL_TIME = 0;
 
     /**
     * 柔性时间回溯
     */
    private static final long FLEXIBILIT_TIME = 5000;
 
    /**
     * 数子量级
     */
    private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
 
    private TOTP() {
    }
 
    /**
     * 生成一次性密码
     *
     * @param code 账户
     * @param pass 密码
     * @return String
     */
    public static String generateMyTOTP(String code, String pass) {
        if (EmptyUtil.isEmpty(code) || EmptyUtil.isEmpty(pass)) {
            throw new RuntimeException("账户密码不许为空");
        }
        long now = new Date().getTime();
        String time = Long.toHexString(timeFactor(now)).toUpperCase();
        return generateTOTP(code + pass + SECRET_KEY, time);
    }
 
    /**
     * 刚性口令验证
     *
     * @param code 账户
     * @param pass 密码
     * @param totp 待验证的口令
     * @return boolean
     */
    public static boolean verifyTOTPRigidity(String code, String pass, String totp) {
        return generateMyTOTP(code, pass).equals(totp);
    }
    
    /**
     * 柔性口令验证
     *
     * @param code 账户
     * @param pass 密码
     * @param totp 待验证的口令
     * @return boolean
     */
    public static boolean verifyTOTPFlexibility(String code, String pass, String totp) {
        long now = new Date().getTime();
        String time = Long.toHexString(timeFactor(now)).toUpperCase();
        String tempTotp = generateTOTP(code + pass + SECRET_KEY, time);
        if (tempTotp.equals(totp)) {
            return true;
        }
        String time2 = Long.toHexString(timeFactor(now - FLEXIBILIT_TIME)).toUpperCase();
        String tempTotp2 = generateTOTP(code + pass + SECRET_KEY, time2);
        return tempTotp2.equals(totp);
    }
 
    /**
     * 获取动态因子
     *
     * @param targetTime 指定时间
     * @return long
     */
    private static long timeFactor(long targetTime) {
        return (targetTime - INITIAL_TIME) / STEP;
    }
 
    /**
     * 哈希加密
     *
     * @param crypto   加密算法
     * @param keyBytes 密钥数组
     * @param text     加密内容
     * @return byte[]
     */
    private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey = new SecretKeySpec(keyBytes, "AES");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }
 
    private static byte[] hexStr2Bytes(String hex) {
        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
        byte[] ret = new byte[bArray.length - 1];
        System.arraycopy(bArray, 1, ret, 0, ret.length);
        return ret;
    }
 
    private static String generateTOTP(String key, String time) {
        return generateTOTP(key, time, "HmacSHA1");
    }
 
 
    private static String generateTOTP256(String key, String time) {
        return generateTOTP(key, time, "HmacSHA256");
    }
 
    private static String generateTOTP512(String key, String time) {
        return generateTOTP(key, time, "HmacSHA512");
    }
 
    private static String generateTOTP(String key, String time, String crypto) {
        StringBuilder timeBuilder = new StringBuilder(time);
        while (timeBuilder.length() < 16)
            timeBuilder.insert(0, "0");
        time = timeBuilder.toString();
 
        byte[] msg = hexStr2Bytes(time);
        byte[] k = key.getBytes();
        byte[] hash = hmac_sha(crypto, k, msg);
        return truncate(hash);
    }
 
    /**
     * 截断函数
     *
     * @param target 20字节的字符串
     * @return String
     */
    private static String truncate(byte[] target) {
        StringBuilder result;
        int offset = target[target.length - 1] & 0xf;
        int binary = ((target[offset] & 0x7f) << 24)
                | ((target[offset + 1] & 0xff) << 16)
                | ((target[offset + 2] & 0xff) << 8) | (target[offset + 3] & 0xff);
 
        int otp = binary % DIGITS_POWER[CODE_DIGITS];
        result = new StringBuilder(Integer.toString(otp));
        while (result.length() < CODE_DIGITS) {
            result.insert(0, "0");
        }
        return result.toString();
    }
} 

二次验证 (2FA) Two-Factor Authentication 是指:在用户名/密码之外,增加一个一次性密码的验证方式。表现形式有很多种,一般有:U盾、手机短信验证码、电话语音验证码、APP或系统弹窗、软件实现(验证器APP)。

二次验证要解决的问题是:用户名/密码是相对固定不变的,如果被窃取(偷看/内存破解/键盘记录/拦截数据传输解密),其它人可以冒充身份。二次验证加入了一个一次性密码以后,这个密码使用一次后即失效,即使使用过程中被窃取也不能被拿来再次通过身份验证。

从这样的原理来看,针对二次验证一次性密码的攻击就是要在生成密码到使用密码之间窃取密码,并抢在用户使用之前用于身份验证。所以现在的各种骗术都是要拿到短信验证码(有的是通过手机木马的方式,有的是通过骗取信任的方式)。

安全性

1. iOS手机不越狱、安卓手机不解锁的话,短信验证码还是可靠的。这也是目前手机银行APP采用的方案。

2. 如果出境,手机号的方案就不方便了。还是要靠验证器APP。

验证器APP的原理

1.生成一个密钥,服务器和用户都知道这个密钥。密钥保管好后,以后不再传输;

2.用密钥+当前时间用一个单向算法生成动态密码。

3.服务器通过动态密码间接验证用户是否持有正确的密钥,进而验证身份。

实际应用中密钥需要保证一定的长度,如:BPPC7YHP2KD2BZFGPGMLZBBOUOO4627H,时间同步按30秒刷新一次,算法使用的是HMAC-SHA1,最终的动态密码是6位数字。

第1步中,密钥可以生成得足够复杂,间接解决了用户喜欢使用弱密码的问题。这一步需要在安全的环境中进行,保证密钥不被泄漏。

整个方案叫做TOTP(Time-based One-time Password 基于时间的一次性密码)。算法本身是公开的(https://datatracker.ietf.org/doc/html/rfc6238),所以有很多APP可选,比如:Google Authenticator、Authy、Microsoft Authenticator、Tofu等。不同的APP在细节上有一些不同,比如密钥的备份、同步等。

为了方便第1步的实践,一般网站会让你用APP扫一个二维码,它的内容实际是像这样子的:

otpauth://totp/Cloudflare:bob?secret=P72DGTKGGUAN7GOGZFFGGNCP5BCN3445&issuer=Cloudflare

其中只有密钥部分是关键有用的,其它部分是辅助你记忆的(哪个服务商,哪个用户名)。在大多数情况下,这两个信息足够了。不过中国的特殊国情,比如:Amazon.com和Amazon.cn没有区分开,显示的服务商都是Amazon。

几个常见验证器APP的功能对比

  • Microsoft Authenticator可以编辑服务商信息,不能编辑用户名信息。
  • Tofu这两个信息都可以编辑。
  • Authy只能编辑用户名信息。
  • Google Authenticator只能编辑用户名信息。

开了二次验证后,登录的时候除了用户名/密码以外,还需要输入一次性密码。(有些网站可以设置常用设备,在常用设备上登录可以免去输入二次验证密码的步骤。使用短信验证码的网站一般只会在有可疑登录的时候要求二次验证)如果验证器APP不在身边,或者手机丢了,又或者升级手机系统把验证器APP保存的密钥删除了,就会很麻烦。所以开了二次验证以后,密钥的备份就很重要。

Authy是备份在它的服务器上的。用手机号+主密码验证身份。有的人认为它的服务器集中存放了很多人的密钥,容易被重点攻击,不够安全。
Keepass有TOTP的插件,会把密钥和设置(刷新时间间隔、动态密钥长度)保存在一条记录的属性里面。这样即使换了一个环境,没有插件,也可以拿到密钥再计算出动态密码。

Keepass已经保存了用户名/密码,如果再把TOTP密钥保存在里面,万一Keepass数据库的主密码被破,就完全城门大开了。所以TOTP和用户名/密码要分别保存在不同的密码库里,使用不同的主密码,甚至密码库也要分开保存。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
TOTP(Time-Based One-Time Password)是一种基于时间的一次性密码算法,用于生成动态密码。它是基于HOTP(HMAC-Based One-Time Password)算法的扩展,通过结合时间戳和密钥生成密码,以提供更高的安全性。 以下是使用uniapp实现TOTP算法的示例代码: ```javascript // 导入js库 import jsSHA from 'jssha'; // 生成TOTP密码 function generateTOTP(secretKey) { // 获取当前时间戳 const timestamp = Math.floor(Date.now() / 1000); // 将时间戳转换为8字节的大端字节数组 const timeBytes = new Array(8); for (let i = 7; i >= 0; i--) { timeBytes[i] = timestamp & 0xff; timestamp >>>= 8; } // 使用HMAC-SHA1算法计算哈希值 const shaObj = new jsSHA('SHA-1', 'ARRAYBUFFER'); shaObj.setHMACKey(secretKey, 'ARRAYBUFFER'); shaObj.update(new Uint8Array(timeBytes)); const hmac = shaObj.getHMAC('ARRAYBUFFER'); // 获取哈希值的最后一个字节 const offset = hmac[hmac.length - 1] & 0xf; // 从哈希值中截取4个字节作为动态密码 const otp = ( ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff) ) % 1000000; // 将动态密码转换为6位字符串 const otpString = otp.toString().padStart(6, '0'); return otpString; } // 使用示例 const secretKey = 'JBSWY3DPEHPK3PXP'; // 密钥 const totp = generateTOTP(secretKey); console.log('TOTP密码:', totp); ``` 请注意,上述代码中使用了`jssha`库来计算HMAC-SHA1哈希值。在使用之前,需要先安装该库。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值