動(dòng)態(tài)密碼TOTP的Java實(shí)現(xiàn)

一、HOTP

??HOTP 算法,全稱是“An HMAC-Based One-Time Password Algorithm”,是一種基于事件計(jì)數(shù)的一次性密碼生成算法精拟,詳細(xì)的算法介紹可以查看 RFC 4226谨读。其實(shí)算法本身非常簡(jiǎn)單哆姻,算法本身可以用兩條簡(jiǎn)短的表達(dá)式描述:

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 算法實(shí)現(xiàn),核心是將移動(dòng)因子從 HOTP 中的事件計(jì)數(shù)改為時(shí)間差。完整的 TOTP 算法的說(shuō)明可以查看 RFC 6238呀酸,其公式描述也非常簡(jiǎn)單:

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.

參考《https://segmentfault.com/a/1190000008394200
這篇文章已經(jīng)介紹清楚動(dòng)態(tài)密碼的原理,本文僅在理論的基礎(chǔ)上使用Java實(shí)現(xiàn)。

三羞海、TOTP具體Java實(shí)現(xiàn)

??子服務(wù)端:?jiǎn)⒂貌缓瑅erifyTOTP*()驗(yàn)證方法的TOTP院水,使用子服務(wù)端的賬戶和密碼加密恢恼,向驗(yàn)證服務(wù)端發(fā)送動(dòng)態(tài)口令。
??驗(yàn)證服務(wù)端:驗(yàn)證子服務(wù)端的口令锁保,根據(jù)子服務(wù)端標(biāo)識(shí)信息,使用其賬戶和密碼加密得到口令心墅,比對(duì)口令是否一致即可。
??前提:因?yàn)槭褂脮r(shí)間作為動(dòng)態(tài)因子加密口令,子服務(wù)端的時(shí)間應(yīng)和驗(yàn)證服務(wù)端的時(shí)間一致,比如使用同一個(gè)授時(shí)服務(wù)器授時(shí)帘睦。
??推薦使用柔性口令驗(yàn)證,使用柔性驗(yàn)證時(shí)請(qǐng)?jiān)O(shè)置時(shí)間回溯參數(shù)逸绎,避免因口令在網(wǎng)絡(luò)中傳輸消耗時(shí)間颊乘,或者服務(wù)端時(shí)間誤差導(dǎo)致口令失效开呐。

/**
 * <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>
 *
 * @author wangqian
 * @date 2018-04-03 11:44
 */
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";

    /**
     * 時(shí)間步長(zhǎng) 單位:毫秒 作為口令變化的時(shí)間周期
     */
    private static final long STEP = 30000;

    /**
     * 轉(zhuǎn)碼位數(shù) [1-8]
     */
    private static final int CODE_DIGITS = 8;

    /**
     * 初始化時(shí)間
     */
    private static final long INITIAL_TIME = 0;

     /**
     * 柔性時(shí)間回溯
     */
    private static final long FLEXIBILIT_TIME = 5000;

    /**
     * 數(shù)子量級(jí)
     */
    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);
    }

    /**
     * 剛性口令驗(yàn)證
     *
     * @param code 賬戶
     * @param pass 密碼
     * @param totp 待驗(yàn)證的口令
     * @return boolean
     */
    public static boolean verifyTOTPRigidity(String code, String pass, String totp) {
        return generateMyTOTP(code, pass).equals(totp);
    }
    
    /**
     * 柔性口令驗(yàn)證
     *
     * @param code 賬戶
     * @param pass 密碼
     * @param totp 待驗(yàn)證的口令
     * @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);
    }

    /**
     * 獲取動(dòng)態(tài)因子
     *
     * @param targetTime 指定時(shí)間
     * @return long
     */
    private static long timeFactor(long targetTime) {
        return (targetTime - INITIAL_TIME) / STEP;
    }

    /**
     * 哈希加密
     *
     * @param crypto   加密算法
     * @param keyBytes 密鑰數(shù)組
     * @param text     加密內(nèi)容
     * @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);
    }

    /**
     * 截?cái)嗪瘮?shù)
     *
     * @param target 20字節(jié)的字符串
     * @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();
    }
} 

歡迎到我的GitHub交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子冯袍,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異畦木,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)垒迂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門欢揖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)她混,“玉大人臭脓,你說(shuō)我怎么就攤上這事佃扼。” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵冗恨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我味赃,道長(zhǎng)掀抹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任心俗,我火速辦了婚禮傲武,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘另凌。我一直安慰自己谱轨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布吠谢。 她就那樣靜靜地躺著土童,像睡著了一般。 火紅的嫁衣襯著肌膚如雪工坊。 梳的紋絲不亂的頭發(fā)上献汗,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天敢订,我揣著相機(jī)與錄音,去河邊找鬼罢吃。 笑死楚午,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尿招。 我是一名探鬼主播矾柜,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼就谜!你這毒婦竟也來(lái)了怪蔑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤丧荐,失蹤者是張志新(化名)和其女友劉穎缆瓣,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虹统,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弓坞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了车荔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渡冻。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖夸赫,靈堂內(nèi)的尸體忽然破棺而出菩帝,到底是詐尸還是另有隱情,我是刑警寧澤茬腿,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布呼奢,位于F島的核電站,受9級(jí)特大地震影響切平,放射性物質(zhì)發(fā)生泄漏握础。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一悴品、第九天 我趴在偏房一處隱蔽的房頂上張望禀综。 院中可真熱鬧,春花似錦苔严、人聲如沸定枷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)欠窒。三九已至,卻和暖如春退子,著一層夾襖步出監(jiān)牢的瞬間岖妄,已是汗流浹背型将。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荐虐,地道東北人七兜。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像福扬,于是被迫代替她去往敵國(guó)和親腕铸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容