TOTP算法,Luhn 解析

Luhn算法简述

Luhn算法,一般用于验证身份识别码,比如国际移动设备辨识码(IMEI),商标识别码。它不是一种安全的加密hash函数,设计的目的只是防止意外出错而不是恶意攻击

通过校验码对一串数字进行验证,校验码通常会被加到这串数字的末尾处,从而得到一个完整的身份识别码

==78 19032121 3233 X==

  • 1-2位:识别码,固定数字XX(2),区别于其他支付码,

    比如微信10-15开头(18位长度),支付宝25-30开头(16-24位)

    云闪付62开头(16-19位)

  • 3-10位:TOTP算法,60秒刷新一次(8位)

  • 11-14位:随机数四位,通过获取登录用户的随机数在缓存中校验

  • 15-17位: 会员id十进制转八进制后截取最后三位,校验用户(3位)

  • 18位:luhn算法,付款码正确性校验(1位)

Luhn算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static char getCheckCode(String nonCheckCode) {
//43558784876107262
char[] chs = nonCheckCode.trim().toCharArray();
int luhmSum = 0;
//i从最后一位开始
//j从第一位开始
for (int i = chs.length - 1, j = 0; i >= 0; i--, j++) {
int k = chs[i] - '0';
if (j % 2 == 0) {
//如果为奇数位,则*2
//从最后一位开始,校验的时候为(包含校验位)偶数位,要得到校验位则是奇数位,也就是除了校验位第一位
k *= 2;
//如果是俩位数则十位数+个位数(-9)
k = k / 10 + k % 10;
}
//其他位不做处理,将所有值相加
luhmSum += k;
}
//示例43558784876107262
//2->4 2->4 0->0 6->3 8->7 8->7 8->7 5->1 4->8
//4+4+0+3+7+7+7+1+8+6+7+1+7+4+7+5+3=81
//如果为0取0,10-luhmSum%10得到最后校验位
//10-81%10=9
//得到的校验位是9 所以正确号码为435587848761072629
return (luhmSum % 10 == 0) ? '0' : (char) ((10 - luhmSum % 10) + '0');
}

TOTP算法:

基于时间戳算法的一次性密码,是时间同步,基于客户端的动态口令和动态口令验证服务器的时间比对,一般60秒 ,或者30,能够十分精确的保持正确的时钟,客户端和服务端基于时间计算的动态口令才能一致。

  • 服务器登录动态密码验证
  • 公司VPN登录双因素验证
  • 银行转账动态密码
  • 网银、网络游戏的实体动态口令牌
  • 等基于时间有效性验证的应用场景

totp计算公式

1
TOTP(K, TC) = Truncate(HMAC-SHA-1(K, TC))

k密匙串,TC是一个函数,用于截取加密后的字符串

TC计算公式

1
TC = (T - T0) / T1

T ,当前时间戳,T0,起始时间,时间间隔,业务自定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 共享密钥
*/
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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 生成一次性密码
*
* @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);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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();
}
}