Exploring
首页
  • Java

    • 面向对象的思想OOP
    • 浅谈Java反射原理
    • endorsed覆盖JDK中的类
  • 认证与授权

    • LDAP概念和原理介绍
    • OAuth2介绍
  • Impala

    • Impala 介绍
  • MySQL

    • 关于MySQL的一些面试题
    • 解决MySQL不到中文数据
    • 数据库之事务与实现原理
  • Oracle

    • oracle的表空间,用户管理,表操作,函数
    • oracle的查询、视图、索引
    • plsql简单入门
  • Redis

    • 数据类型详解
    • 跳越表
    • 数据持久化的两种方式
  • 共识算法

    • gossip
  • RPC

    • GRPC初识与快速入门
    • ProtocolBuffer基本语法
  • RabbitMQ

    • RabbitMQ入门程序之HelloWorld
    • RabbitMQ之工作模式
  • Zookeeper

    • Zookeeper一文入门
  • Docker

    • Docker入门初体验
  • Maven

    • 把自己的包到Maven中央仓库
    • Maven之自定义插件
  • Nginx

    • nginx的安装
    • nginx的配置文件
    • nignx 的变量
  • Tomcat

    • Servlet3通过SPI进行注册组件
  • Vagrant

    • vagrant 初始化
    • vagrant 常用配置
    • vagrant 自己制作 box
  • Linux

    • 启动方式 Systemd
    • 后台服务
    • 防火墙与 Iptables
  • 设计模式

    • 设计模式-代理
    • 设计模式-单例模式
    • 设计模式-迭代器
  • 分布式

    • CAP 理论
  • 数据结构

    • 数据结构之堆Heap
    • 数据结构之哈希表
    • 数据结构之队列
  • 计算机网络

    • HTTP与HTTPS详解
    • 浅谈DNS协议
    • ISP中的网络层
  • 算法

    • 常用查找算法及Java实现
    • 常用排序算法及Java实现
    • 迪杰斯特拉算法
  • 操作系统

    • 操作系统之进程调度算法
    • 操作系统之进程通讯IPC
    • 操作系统之内存管理
  • 抓包

    • 生成安卓系统证书
  • 加解密

    • 常见加密算法
    • 公开秘钥基础知识
    • RSA 解析
  • Windows

    • scoop 包管理
    • windows-terminal 配置
    • 增强 PowerShell
归档
Github (opens new window)
首页
  • Java

    • 面向对象的思想OOP
    • 浅谈Java反射原理
    • endorsed覆盖JDK中的类
  • 认证与授权

    • LDAP概念和原理介绍
    • OAuth2介绍
  • Impala

    • Impala 介绍
  • MySQL

    • 关于MySQL的一些面试题
    • 解决MySQL不到中文数据
    • 数据库之事务与实现原理
  • Oracle

    • oracle的表空间,用户管理,表操作,函数
    • oracle的查询、视图、索引
    • plsql简单入门
  • Redis

    • 数据类型详解
    • 跳越表
    • 数据持久化的两种方式
  • 共识算法

    • gossip
  • RPC

    • GRPC初识与快速入门
    • ProtocolBuffer基本语法
  • RabbitMQ

    • RabbitMQ入门程序之HelloWorld
    • RabbitMQ之工作模式
  • Zookeeper

    • Zookeeper一文入门
  • Docker

    • Docker入门初体验
  • Maven

    • 把自己的包到Maven中央仓库
    • Maven之自定义插件
  • Nginx

    • nginx的安装
    • nginx的配置文件
    • nignx 的变量
  • Tomcat

    • Servlet3通过SPI进行注册组件
  • Vagrant

    • vagrant 初始化
    • vagrant 常用配置
    • vagrant 自己制作 box
  • Linux

    • 启动方式 Systemd
    • 后台服务
    • 防火墙与 Iptables
  • 设计模式

    • 设计模式-代理
    • 设计模式-单例模式
    • 设计模式-迭代器
  • 分布式

    • CAP 理论
  • 数据结构

    • 数据结构之堆Heap
    • 数据结构之哈希表
    • 数据结构之队列
  • 计算机网络

    • HTTP与HTTPS详解
    • 浅谈DNS协议
    • ISP中的网络层
  • 算法

    • 常用查找算法及Java实现
    • 常用排序算法及Java实现
    • 迪杰斯特拉算法
  • 操作系统

    • 操作系统之进程调度算法
    • 操作系统之进程通讯IPC
    • 操作系统之内存管理
  • 抓包

    • 生成安卓系统证书
  • 加解密

    • 常见加密算法
    • 公开秘钥基础知识
    • RSA 解析
  • Windows

    • scoop 包管理
    • windows-terminal 配置
    • 增强 PowerShell
归档
Github (opens new window)
  • 抓包

  • 加解密

    • 常见加密算法
    • 公开秘钥基础知识
    • RSA 解析
    • ECC 与 SM2
    • 加签验签
    • 对称加密
    • 一次性密码 OTP
      • 概念
      • 形式
        • 时间同步
        • 事件同步
        • 挑战/应答
      • OTP基本原理
        • HOTP原理
        • TOTP原理
      • 代码实现
        • HOTP
        • TOTP
      • 拓展阅读 2FA 与 2SV
        • 身份认证三要素
        • 2SV 两步验证(Two Steps Verification)
        • 2FA 双因素认证(Two Factor Authentication)
        • 总结一下
    • 数字证书
  • 安全
  • 加解密
unclezs
2022-05-18
0
目录

一次性密码 OTP

# 概念

一次性密码(One Time Password,简称OTP),又称“一次性口令”,是指只能使用一次的密码。一次性密码是根据专门算法、每隔60秒生成一个不可预测的随机数字组合,iKEY一次性密码已在金融 (opens new window)、电信 (opens new window)、网游 (opens new window)等领域被广泛应用,有效地保护了用户的安全。

一般的静态密码在安全性上容易因为木马 (opens new window)与键盘侧录程序 (opens new window)等而被窃取,而只要花上相当程度的时间,也有可能被暴力破解 (opens new window)。为了解决一般密码容易遭到破解情况,因此开发出一次性密码的解决方案。

在平时生活中,我们接触一次性密码的场景非常多,比如在登录账号、找回密码,更改密码和转账操作等等这些场景,其中一些常用到的方式有:

  • 手机短信+短信验证码;
  • 邮件+邮件验证码;
  • 认证器软件+验证码,比如Microsoft Authenticator App,Google Authenticator App等等;
  • 硬件+验证码:比如网银的电子密码器;

这些场景的流程一般都是在用户提供了账号+密码的基础上,让用户再提供一个一次性的验证码来提供一层额外的安全防护。通常情况下,这个验证码是一个6-8位的数字,只能使用一次或者仅在很短的时间内可用(比如5分钟以内)

# 形式

OTP从技术来分有三种形式, 时间同步、事件同步、挑战/应答。

# 时间同步

原理是基于 动态令牌和 动态口令验证服务器的时间比对,基于 时间同步的 令牌,一般每60秒产生一个新口令,要求服务器能够十分精确的保持正确的时钟,同时对其令牌的晶振频率有严格的要求,这种技术对应的终端是硬件令牌。

# 事件同步

基于事件同步的令牌,其原理是通过某一特定的事件次序及相同的种子值作为输入,通过HASH算法中运算出一致的密码。

# 挑战/应答

常用于的网上业务,在网站/应答上输入 服务端下发的 挑战码, 动态令牌输入该挑战码,通过内置的算法上生成一个6/8位的随机数字,口令一次有效,这种技术目前应用最为普遍,包括刮刮卡、短信密码、动态令牌也有挑战/应答形式。 主流的动态令牌技术是时间同步和挑战/应答两种形式。

# OTP基本原理

image-20220518115712586

计算OTP串的公式

OTP(K,C) = Truncate(HMAC-SHA-1(K,C))
1
  • K: 表示秘钥串,这个密钥的要求是每个 HOTP 的生成器都必须是唯一的。一般我们都是通过一些随机生成种子的库来实现。
  • C: RFC 中把它称为移动元素(moving factor)是一个 8个 byte的数值,而且需要服务器和客户端同步。
  • HMAC-SHA-1: 表示使用SHA-1做HMAC;
  • Truncate: 是一个函数,就是怎么截取加密后的串,并取加密后串的哪些字段组成一个数字。

# HOTP原理

HOTP(HMAC-Based One Time Password) 即是基于 HMAC(基于Hash的消息认证码)实现的一次性密码。算法细节定义在RFC4226 (opens new window).

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

一般规定 HOTP 的散列函数使用 SHA2,即:基于SHA-256 或者SHA-512[SHA2 (opens new window)] 的散列函数做事件同步验证。

image-20220518145554591

步骤文解:

  1. 使用 HMAC-SHA-1 算法基于 K 和 C 生成一个20个字节的十六进制字符串(HS)。关于如何生成这个是另外一个协议来规定的,RFC 2104 HMAC Keyed-Hashing for Message Authentication (opens new window). 实际上这里的算法并不唯一,还可以使用 HMAC-SHA-256 和 HMAC-SHA-512 生成更长的序列。对应到协议中的算法标识就是

    HS = HMAC-SHA-1(K,C)
    
    1
  2. 选择这个20个字节的十六进制字符串(HS 下文使用 HS 代替 )的最后一个字节,取其低4位数并转化为十进制。比如图中的例子,第二个字节是 5a,第四位就是 a,十六进制也就是 0xa,转化为十进制就是 10。该数字我们定义为 Offset,也就是偏移量。

  3. 根据偏移量 Offset,我们从 HS 中的第 10(偏移量)个字节开始选取 4 个字节,作为我们生成 OTP 的基础数据。图中例子就是选择 50ef7f19,十六进制表示就是 0x50ef7f19,我们称为 Sbits

  4. 将上一步4个字节的十六进制字符串 Sbits 转化为十进制,然后用该十进制数对 10的Digit次幂 进行取模运算。其原理很简单根据取模运算的性质,比如 比10大的数 MOD 10 结果必然是 0到9, MOD 100 结果必然是 0-99。图中的例子,50ef7f19 转化为十进制为 1357872921,然后如果需要6位 OTP 验证码,则 1357872921 MOD 10^6 = 872921。 872921 就是我们最终生成的 OTP。

    这一步可能还需要注意一点就是图中案例 Digit 不能超过10,因为即使超过10,1357872921 取模后也不会超过10位了。所以如果我们希望获取更长的验证码,需要在三步中拿到更多的十六进制字节,从而得到更大的十进制数。这个十进制决定了生成的 OTP 编码的长度。

# TOTP原理

TOTP 算法的关键在于如何更具当前时间和时间窗口计算出计数,也就是如何根据当前时间和 X 来计算 HOTP 算法中的 C。

image-20220518150359721 HOTP 算法中的 C 是使用当前 Unix 时间戳 减去初始计数时间戳,然后除以时间窗口而获得的。

C = (T - T0) / X;
1
  • T 为当前时间
  • T0 从哪个时间开始,一般取值为 0.
  • X 表示时间步数,也就是说多长时间产生一个动态密码,这个时间间隔就是时间步数X,比如30秒;

# 代码实现

# HOTP

package com.unclezs.samples.crypto.otp;

import cn.hutool.core.codec.Base32;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;

import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicInteger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


/**
 * HOTP 规范实现 <a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC6238</a>
 * <pre>
 * TOTP(K,C) = HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
 * </pre>
 *
 * @author blog.unclezs.com
 * @date 2022/5/18 1:58 PM
 */
public class HOTPSample {
  /**
   * 用户密钥长度 80bit
   */
  private static final int SECRET_KEY_BITS = 80;
  /**
   * TOTP 长度 : 6
   */
  private static final int CODE_DIGITS = 6;
  /**
   * 有效视窗长度 最多计数器比本端小 10
   */
  private static final int WINDOW_SIZE = 10;
  /**
   * 伪随机函数生成算法 sha1 (pseudorandom number generator)
   */
  public static final String PRNG_ALGORITHM = "SHA1PRNG";
  /**
   * 加密算法 Hmac
   */
  public static final String CRYPTO_ALGORITHM = "HmacSHA1";
  /**
   * 二维码 url 格式
   **/
  public static final String QRCODE_URL = "otpauth://totp/%s:%s?secret=%s&issuer=%s";
  /**
   * C 的 byte 数
   */
  public static final int COUNTER_BITS = 8;
  /**
   * 模拟计数器
   */
  private static final AtomicInteger COUNTER = new AtomicInteger(0);

  /**
   * 创建密钥
   *
   * @return {@link String}
   */
  @SneakyThrows
  public String createSecretKey() {
    // 使用 SecureRandom 产生安全的随机数
    SecureRandom keyRandom = SecureRandom.getInstance(PRNG_ALGORITHM);
    byte[] keyBytes = keyRandom.generateSeed(SECRET_KEY_BITS / 8);
    // 将随机数进行 Base32 编码,产生一个随机字符串密钥
    return Base32.encode(keyBytes);
  }

  /**
   * HmacSha1算法对窗口(C)加密
   *
   * @param secretKeyByte   密钥字节
   * @param timeWindowBytes 窗字节
   * @return {@link byte[]}
   */
  private byte[] hmacSha1(byte[] secretKeyByte, byte[] timeWindowBytes) {
    SecretKeySpec keySpec = new SecretKeySpec(secretKeyByte, CRYPTO_ALGORITHM);
    try {
      // 使用 HmacSHA1 算法,返回一个 160 bit 的 hash 值
      Mac keyMac = Mac.getInstance(CRYPTO_ALGORITHM);
      keyMac.init(keySpec);
      return keyMac.doFinal(timeWindowBytes);
    } catch (GeneralSecurityException e) {
      e.printStackTrace();
      throw new UndeclaredThrowableException(e);
    }
  }

  /**
   * 获得8 字节的窗口(X)
   *
   * @param window 窗口(X)
   * @return {@link byte[]}
   */
  private byte[] getCounter(int window) {
    // 将 window 转为 byte 数组
    byte[] counterBytes = new byte[COUNTER_BITS];
    for (int i = COUNTER_BITS; i-- > 0; window >>>= 8) {
      // 进行截断赋值
      counterBytes[i] = (byte) window;
    }
    return counterBytes;
  }

  /**
   * 获得窗口(X)
   *
   * @return long
   */
  private int getCurrentWindow() {
    return COUNTER.get();
  }

  /**
   * 生成 TOTP
   *
   * @param key 秘钥 (K)
   * @param c   计数器
   * @return {@link String}
   */
  public String generateOtp(String key, byte[] c) {
    byte[] keyBytes = Base32.decode(key);
    byte[] hash = hmacSha1(keyBytes, c);

    // offset : 开始取字节的位置; 由于 HmacSHA1算法返回的是160bit,也就是 20 byte, 所以 hash 长度是 20, 用hash
    // 的最后一位和 0xF 做 & 操作,使 0 <= offset <= 15, 这样即使 offset 为 15 ,连续 4
    // 次取字节,最多取到hash[18],不会发生数组越界
    int offset = hash[hash.length - 1] & 0xF;

    // 从 hash 中连续取出 4 个字节(32bit),将其组成一个 int 型正整数, 进行了 4 次操作,分别是将 4个 字节移到 originOtp
    // 的第 1,2,3,4 字节
    // (hash[offset] & 0x7F) 则是为了 originOtp 的首位是 0, 可以得到一个正数
    int originOtp =
        ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16) | ((hash[offset + 2] & 0xFF) << 8) | (
            hash[offset + 3] & 0xFF);

    // google 中 codeDigits 为 6,表示得到的 totp 长度是 6
    // 对 10^6 取余,得到的余数的长度一定不大于 6
    int totp = originOtp % (int) Math.pow(10, CODE_DIGITS);

    // 如果得到的余数 totp 长度小于 6 ,则在前面补 0
    StringBuilder resultTotp = new StringBuilder(Integer.toString(totp));
    while (resultTotp.length() < CODE_DIGITS) {
      resultTotp.insert(0, "0");
    }
    return resultTotp.toString();
  }

  /**
   * 得到当前otp
   *
   * @param secretKey 秘密密钥
   * @return {@link String}
   */
  public String getCurrentOtp(String secretKey) {
    String otp = generateOtp(secretKey, getCounter(getCurrentWindow()));
    // 计数器自增
    COUNTER.incrementAndGet();
    return otp;
  }

  /**
   * 检查 TOTP
   *
   * @param secretKey  秘密密钥
   * @param targetTotp TOTP
   * @return boolean
   */
  public boolean checkTotp(String secretKey, String targetTotp) {
    int currentWindow = getCurrentWindow();
    for (int i = 0; i <= WINDOW_SIZE; i++) {
      String totp = generateOtp(secretKey, getCounter(currentWindow + i));
      if (StrUtil.equals(targetTotp, totp)) {
        return true;
      }
    }
    return false;
  }

  /**
   * 生成二维码
   *
   * @param secretKey 秘密密钥
   * @param username  用户名
   * @param issuer    发行人
   * @param prefix    前缀
   * @return {@link String}
   */
  public String generateQrCode(String secretKey, String username, String issuer, String prefix) {
    return String.format(QRCODE_URL, prefix, username, secretKey, issuer);
  }

  public static void main(String[] args) {
    HOTPSample authenticator = new HOTPSample();
    String secretKey = authenticator.createSecretKey();
    // secretKey = "RKJQRUEZC22LWHXK";
    String otp = authenticator.getCurrentOtp(secretKey);
    System.out.println(secretKey);
    System.out.println(otp);
    // 模拟计数器同步
    COUNTER.decrementAndGet();
    System.out.println(authenticator.checkTotp(secretKey, otp));
    COUNTER.incrementAndGet();
  }
}
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

输出

2AES7T4TOH57QXUP
230078
true
1
2
3

# TOTP

package com.unclezs.samples.crypto.otp;

import cn.hutool.core.codec.Base32;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;

import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


/**
 * TOTP google 规范实现 <a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC6238</a>
 * <pre>
 * TOTP(K,C) = HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
 * C = (T - T0) / X;
 * T0 = 0
 * </pre>
 *
 * @author blog.unclezs.com
 * @date 2022/5/18 1:58 PM
 */
public class TOTPSample {
  /**
   * 用户密钥长度 80bit
   */
  private static final int SECRET_KEY_BITS = 80;
  /**
   * TOTP 长度 : 6
   */
  private static final int CODE_DIGITS = 6;
  /**
   * 有效视窗长度 : (-2,2), 也就是服务器时间之间 可以早 1 分钟或者晚了 1 分钟
   */
  private static final int WINDOW_SIZE = 2;
  /**
   * TOTP更新周期 : 30s 更新一次
   */
  private static final int TIME_STEP = 30;
  /**
   * 伪随机函数生成算法 sha1 (pseudorandom number generator)
   */
  public static final String PRNG_ALGORITHM = "SHA1PRNG";
  /**
   * 加密算法 Hmac
   */
  public static final String CRYPTO_ALGORITHM = "HmacSHA1";
  /**
   * 二维码 url 格式
   **/
  public static final String QRCODE_URL = "otpauth://totp/%s:%s?secret=%s&issuer=%s";
  /**
   * C 的 byte 数
   */
  public static final int COUNTER_BITS = 8;

  /**
   * 创建密钥
   *
   * @return {@link String}
   */
  @SneakyThrows
  public String createSecretKey() {
    // 使用 SecureRandom 产生安全的随机数
    SecureRandom keyRandom = SecureRandom.getInstance(PRNG_ALGORITHM);
    byte[] keyBytes = keyRandom.generateSeed(SECRET_KEY_BITS / 8);
    // 将随机数进行 Base32 编码,产生一个随机字符串密钥
    return Base32.encode(keyBytes);
  }

  /**
   * HmacSha1算法对时间窗口(C)加密
   *
   * @param secretKeyByte   密钥字节
   * @param timeWindowBytes 时间窗字节
   * @return {@link byte[]}
   */
  private byte[] hmacSha1(byte[] secretKeyByte, byte[] timeWindowBytes) {
    SecretKeySpec keySpec = new SecretKeySpec(secretKeyByte, CRYPTO_ALGORITHM);
    try {
      // 使用 HmacSHA1 算法,返回一个 160 bit 的 hash 值
      Mac keyMac = Mac.getInstance(CRYPTO_ALGORITHM);
      keyMac.init(keySpec);
      return keyMac.doFinal(timeWindowBytes);
    } catch (GeneralSecurityException e) {
      e.printStackTrace();
      throw new UndeclaredThrowableException(e);
    }
  }

  /**
   * 获得8 字节的时间窗口(X)
   *
   * @param timeWindow 时间窗口(X)
   * @return {@link byte[]}
   */
  private byte[] getCounter(long timeWindow) {
    // 将 timeWindow 转为 byte 数组
    byte[] timeWindowBytes = new byte[COUNTER_BITS];
    for (int i = COUNTER_BITS; i-- > 0; timeWindow >>>= 8) {
      // 进行截断赋值
      timeWindowBytes[i] = (byte) timeWindow;
    }
    return timeWindowBytes;
  }

  /**
   * 获得时间窗口(X)
   *
   * @return long
   */
  private long getCurrentWindow() {
    return System.currentTimeMillis() / TimeUnit.SECONDS.toMillis(TIME_STEP);
  }

  /**
   * 生成 TOTP
   *
   * @param key 秘钥 (K)
   * @param c   计数器
   * @return {@link String}
   */
  public String generateOtp(String key, byte[] c) {
    byte[] keyBytes = Base32.decode(key);
    byte[] hash = hmacSha1(keyBytes, c);

    // offset : 开始取字节的位置; 由于 HmacSHA1算法返回的是160bit,也就是 20 byte, 所以 hash 长度是 20, 用hash
    // 的最后一位和 0xF 做 & 操作,使 0 <= offset <= 15, 这样即使 offset 为 15 ,连续 4
    // 次取字节,最多取到hash[18],不会发生数组越界
    int offset = hash[hash.length - 1] & 0xF;

    // 从 hash 中连续取出 4 个字节(32bit),将其组成一个 int 型正整数, 进行了 4 次操作,分别是将 4个 字节移到 originOtp
    // 的第 1,2,3,4 字节
    // (hash[offset] & 0x7F) 则是为了 originOtp 的首位是 0, 可以得到一个正数
    int originOtp =
        ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16) | ((hash[offset + 2] & 0xFF) << 8) | (
            hash[offset + 3] & 0xFF);

    // google 中 codeDigits 为 6,表示得到的 totp 长度是 6
    // 对 10^6 取余,得到的余数的长度一定不大于 6
    int totp = originOtp % (int) Math.pow(10, CODE_DIGITS);

    // 如果得到的余数 totp 长度小于 6 ,则在前面补 0
    StringBuilder resultTotp = new StringBuilder(Integer.toString(totp));
    while (resultTotp.length() < CODE_DIGITS) {
      resultTotp.insert(0, "0");
    }
    return resultTotp.toString();
  }

  /**
   * 检查 TOTP
   *
   * @param secretKey  秘密密钥
   * @param targetTotp TOTP
   * @return boolean
   */
  public boolean checkTotp(String secretKey, String targetTotp) {
    long currentWindow = getCurrentWindow();
    for (int i = -WINDOW_SIZE; ++i < WINDOW_SIZE; ) {
      String totp = generateOtp(secretKey, getCounter(currentWindow + i));
      if (StrUtil.equals(targetTotp, totp)) {
        return true;
      }
    }
    return false;
  }

  /**
   * 生成二维码
   *
   * @param secretKey 秘密密钥
   * @param username  用户名
   * @param issuer    发行人
   * @param prefix    前缀
   * @return {@link String}
   */
  public String generateQrCode(String secretKey, String username, String issuer, String prefix) {
    return String.format(QRCODE_URL, prefix, username, secretKey, issuer);
  }

  public static void main(String[] args) {
    TOTPSample authenticator = new TOTPSample();
    String secretKey = authenticator.createSecretKey();
    // secretKey = "RKJQRUEZC22LWHXK";
    String totp = authenticator.generateOtp(secretKey, authenticator.getCounter(authenticator.getCurrentWindow()));
    System.out.println(secretKey);
    System.out.println(totp);
    System.out.println(authenticator.checkTotp(secretKey, totp));
  }
}
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195

# 拓展阅读 2FA 与 2SV

# 身份认证三要素

首先解释下什么是身份认证?其实很简单,就是让对方相信你就是你。那么如何让对方相信你就是你呢?按照你能提供的信息的等级来划分,大致有如下三种信息可以证明你就是你自己:

  1. 你所知道的信息:比如我们最广泛使用的“用户名+密码”,因为只有你自己知道“用户名+密码”这个信息组合,那么当你把这个组合提供给我的时候,我就可以相信你就是你。
  2. 你所拥有的信息:假如你的“用户名+密码”泄露给了第三方,这个时候你就会有被第三方冒充的危险了。怎么办呢,再进一步提供一个只有你自己拥有的信息,即可防止被第三方冒充的危险。
  3. 你所独有的信息:再假设一下,你拥有的信息也被泄露给了第三方,这个时候你又会面临被冒充的危险。再进一步,提供一个只有你自己所独有的的信息,比如你的指纹,虹膜,面部特征等等。

# 2SV 两步验证(Two Steps Verification)

两步验证现在是一个再加强认证安全方面广泛使用的一个解决方案。比如Google的2SV (opens new window),Microsoft的2SV (opens new window)等等,通常的做法是当用户输入了"用户名+密码"的基础上,会让用户再提供一个一次性密码(以短信、邮件,或者动态密码生成器app的方式发放给用户)。再有比如在一些服务中需要用户额外设置的安全问题,比如“你的出生地在哪?”等等此类。

# 2FA 双因素认证(Two Factor Authentication)

2SV有个孪生兄弟2FA(双因素认证:Two Factor Authentication),那么关于2SV和2FA有什么区别呢,比如让用户在“用户名+密码”的基础上提供的额外的一次性密码,关于这个一次性密码到底是属于“你所知道的信息”还是“你所拥有的信息”呢?并没有明显的区分界限, 如果你觉得这个一次性密码属于“你所知道的信息”,那么你可以认为它是2SV;如果你觉得这个一次性密码属于“你所拥有的信息”,那么你可以认为它是2FA。

# 总结一下

2SV 的一次性密码就像是由服务商发给你的验证码,这个是需要服务端触发的,不是你随时可以看到的。

2FA 的一次性密码就是通过 OTP 算法生成的,可以在各类离线 APP 中使用,你随时可以看到的。

在 GitHub 编辑此页 (opens new window)
上次更新: 2024/02/25, 12:11:11
对称加密
数字证书

← 对称加密 数字证书→

Theme by Vdoing | Copyright © 2018-2024 unclezs
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式