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