前言:
今天咱们对“java口令”大约比较珍视,同学们都想要分析一些“java口令”的相关知识。那么小编也在网上网罗了一些关于“java口令””的相关内容,希望兄弟们能喜欢,看官们一起来学习一下吧!概念
一次性密码(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基本原理
计算OTP串的公式
OTP(K,C) = Truncate(HMAC-SHA-1(K,C))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))
一般规定 HOTP 的散列函数使用 SHA2,即:基于SHA-256 或者SHA-512[SHA2 (opens new window)] 的散列函数做事件同步验证。
步骤文解:
使用 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选择这个20个字节的十六进制字符串(HS 下文使用 HS 代替 )的最后一个字节,取其低4位数并转化为十进制。比如图中的例子,第二个字节是 5a,第四位就是 a,十六进制也就是 0xa,转化为十进制就是 10。该数字我们定义为 Offset,也就是偏移量。根据偏移量 Offset,我们从 HS 中的第 10(偏移量)个字节开始选取 4 个字节,作为我们生成 OTP 的基础数据。图中例子就是选择 50ef7f19,十六进制表示就是 0x50ef7f19,我们称为 Sbits将上一步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。
HOTP 算法中的 C 是使用当前 Unix 时间戳 减去初始计数时间戳,然后除以时间窗口而获得的。
C = (T - T0) / X;T 为当前时间T0 从哪个时间开始,一般取值为 0.X 表示时间步数,也就是说多长时间产生一个动态密码,这个时间间隔就是时间步数X,比如30秒;#代码实现
<!-- Java OTP 依赖 --> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.3</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.3.3</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency>
GoogleGeneratorUtil
package com.springboot.demo.util;import lombok.extern.slf4j.Slf4j;import org.apache.commons.codec.binary.Base32;import org.apache.commons.codec.binary.Base64;import org.springframework.stereotype.Component;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.util.Objects;@Slf4j@Componentpublic class GoogleGeneratorUtil { // 生成的key长度( Generate secret key length) public static final int SECRET_SIZE = 10; public static final String SEED = "22150146801713967E8g"; // Java实现随机数算法 public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG"; // 最多可偏移的时间 private static int OTP_WINDOW_SIZE = 1; // default 1 - max 17 /** * 创建密钥 * * @return {@link String} */ public static String generateSecretKey() throws NoSuchAlgorithmException { SecureRandom sr = null; try { sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM); sr.setSeed(Base64.decodeBase64(SEED)); byte[] buffer = sr.generateSeed(SECRET_SIZE); Base32 codec = new Base32(); byte[] bEncodedKey = codec.encode(buffer); String encodedKey = new String(bEncodedKey); return encodedKey; } catch (NoSuchAlgorithmException e) { // should never occur... configuration error log.error("otp生成SecretKey失败", e); throw e; } } /** * 这个format不可以修改,身份验证器无法识别二维码 * 二维码 url 格式 * @param user * @param secret * @return */ public static String getQRBarcodeStr(String user, String secret) { String format = "otpauth://totp/Otpdemo:%s?secret=%s&issuer=Otpdemo"; return String.format(format, user, secret); } public static boolean check_code(String secret, String code, long timeMsec) { Base32 codec = new Base32(); byte[] decodedKey = codec.decode(secret); // convert unix msec time into a 30 second "window" // this is per the TOTP spec (see the RFC for details) long t = (timeMsec / 1000L) / 30L; // Window is used to check codes generated in the near past. // You can use this value to tune how far you're willing to go. for (int i = -OTP_WINDOW_SIZE; i <= OTP_WINDOW_SIZE; ++i) { long hash; try { hash = verify_code(decodedKey, t + i); } catch (Exception e) { // Yes, this is bad form - but // the exceptions thrown would be rare and a static // configuration problem// e.printStackTrace (); log.error("失败", e); throw new RuntimeException(e.getMessage(), e); // return false; } if (Objects.equals(code, addZero(hash))) { return true; } } // The validation code is invalid. return false; } public static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException { byte[] data = new byte[8]; long value = t; for (int i = 8; i-- > 0; value >>>= 8) { data[i] = (byte) value; } SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signKey); byte[] hash = mac.doFinal(data); int offset = hash[20 - 1] & 0xF; // We're using a long because Java hasn't got unsigned int. long truncatedHash = 0; for (int i = 0; i < 4; ++i) { truncatedHash <<= 8; // We are dealing with signed bytes: // we just keep the first byte. truncatedHash |= (hash[offset + i] & 0xFF); } truncatedHash &= 0x7FFFFFFF; truncatedHash %= 1000000; return (int) truncatedHash; } public static String addZero(long code) { return String.format("%06d", code); }// @NacosValue(value = "${otp.window.size}", autoRefreshed = true) int otpWindowSize = 1; public void setOtpWindowSize(int otpWindowSize) { if (otpWindowSize > 17 || otpWindowSize < 0) { throw new RuntimeException("otp.window.size最大值是17,最小为1"); } OTP_WINDOW_SIZE = otpWindowSize; }}
QrCodeUtil
package com.springboot.demo.util;import com.google.zxing.BarcodeFormat;import com.google.zxing.EncodeHintType;import com.google.zxing.MultiFormatWriter;import com.google.zxing.WriterException;import com.google.zxing.common.BitMatrix;import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;import lombok.extern.slf4j.Slf4j;import org.apache.tomcat.util.codec.binary.Base64;import javax.imageio.ImageIO;import java.awt.image.BufferedImage;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.util.Hashtable;@Slf4jpublic class QrCodeUtil { private static final int BLACK = 0xFF000000; private static final int WHITE = 0xFFFFFFFF; public static String encode(String contents) throws Exception { try { BufferedImage bufferedImage = toBufferedImage(contents); return writeToBase64(bufferedImage, "png"); } catch (Exception e) { log.error("生成二维码base64字符串失败", e); throw e; } } private static String writeToBase64(BufferedImage bufferedImage, String format) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, format, byteArrayOutputStream); return Base64.encodeBase64String(byteArrayOutputStream.toByteArray()); } private static BufferedImage toBufferedImage(String contents) throws WriterException { Hashtable<EncodeHintType, Object> hintTypeObjectHashtables = new Hashtable<>(); hintTypeObjectHashtables.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); hintTypeObjectHashtables.put(EncodeHintType.CHARACTER_SET, "utf-8"); hintTypeObjectHashtables.put(EncodeHintType.MARGIN, 1); BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, 500, 500, hintTypeObjectHashtables); int width = bitMatrix.getWidth(); int height = bitMatrix.getHeight(); BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { bufferedImage.setRGB(i, j, bitMatrix.get(i, j) == true ? BLACK : WHITE); } } return bufferedImage; }}
测试验证
import com.springboot.demo.util.GoogleGeneratorUtil;import com.springboot.demo.util.QrCodeUtil;import org.apache.commons.codec.binary.Base32;/** * @author li * @version 1.0 * @date 2023/05/11 09:58 */public class testOtp { public static void main(String[] args) throws Exception { //生成秘钥 String secret = GoogleGeneratorUtil.generateSecretKey(); //生成认证url String qrcode = GoogleGeneratorUtil.getQRBarcodeStr("test", secret); //二维码Base64 String qrcodeBase64 = QrCodeUtil.encode(qrcode); System.out.println(qrcode); System.out.println(qrcodeBase64); //生成6位otp验证码 Base32 codec = new Base32(); byte[] decodedKey = codec.decode(secret); long t = (System.currentTimeMillis() / 1000L) / 30L; int realOtpCode = GoogleGeneratorUtil.verify_code(decodedKey,t+1); //校验otp6位验证码 String otpCode = GoogleGeneratorUtil.addZero(realOtpCode); System.out.println("otpCode: "+ otpCode); //校验otp6位数字是否正确 Boolean flag = GoogleGeneratorUtil.check_code(secret,otpCode,System.currentTimeMillis()); System.out.println(flag); }}
标签: #java口令