双因子认证系统登录模块
实现原理:
一、用户需要开启Google Authenticator服务时,
1.服务器随机生成一个类似于『DPI45HKISEXU6HG7』的密钥,并且把这个密钥保存在数据库中。
2.在页面上显示一个二维码,内容是一个URI地址(otpauth://totp/账号?secret=密钥),如『otpauth://totp/kisexu@gmail.com?secret=DPI45HCEBCJK6HG7』,下图:
otpauth://totp/kisexu@gmail.com?secret=DPI45HCEBCJK6HG7 (二维码自动识别)
3.客户端扫描二维码,把密钥『DPI45HKISEXU6HG7』保存在客户端。
二、用户需要登陆时
1.客户端每30秒使用密钥『DPI45HKISEXU6HG7』和时间戳通过一种『算法』生成一个6位数字的一次性密码,如『684060』。如下图android版界面:
2.用户登陆时输入一次性密码『684060』。
3.服务器端使用保存在数据库中的密钥『DPI45HKISEXU6HG7』和时间戳通过同一种『算法』生成一个6位数字的一次性密码。大家都懂控制变量法,如果算法相同、密钥相同,又是同一个时间(时间戳相同),那么客户端和服务器计算出的一次性密码是一样的。服务器验证时如果一样,就登录成功了。
一. 编程思路(实验原理)
使用的是谷歌身份验证器
加盐哈希:存放帐号的数据库经常成为入侵的目标,所以你必须做点什么来保护密码,以防网站被攻破时发生危险。最好的办法就是对密码进行加盐哈希。在密码中混入一段“随机”的字符串再进行哈希加密,这个被字符串被称作盐值。这使得同一个密码每次都被加密为完全不同的字符串。为了校验密码是否正确,我们需要储存盐值。通常和密码哈希值一起存放在账户数据库中,或者直接存为哈希字符串的一部分。
Google身份验证器:客户端和服务器事先协商号一个密钥K,用于一次性密码的生成过程,此密钥不被任何第三方知道,此外,客户端和服务器各有一个计数器C,并且事先将计数值同步,进行验证时,客户端对密钥和计数器的组合(k.c)使用的HMAC(Hash-based Message Authentication Code)
上面采用了HMAC-SHA-1,当然也可以使用HMAC-MD5等。HMAC算法得出的值位数比较多,不方便用户输入,因此需要截断(Truncate)成为一组不太长十进制数(例如6位)。计算完成之后客户端计数器C计数值加1。用户将这一组十进制数输入并且提交之后,服务器端同样的计算,并且与用户提交的数值比较,如果相同,则验证通过,服务器端将计数值C增加1。如果不相同,则验证失败。
谷歌身份验证器
Google-Authenticator默认认证方式为密码+验证码
Google-authenticator是基于时间的一次性密码算法(TOTP)是一种根据预共享的密钥与当前时间计算一次性密码的算法
TOTP是散列消息认证码(HMAC)当中的一个例子。它结合一个私钥与当前时间戳,使用一个密码散列函数来生成一次性密码。由于网络延迟与时钟不同步可能导致密码接收者不得不尝试多次遇到正确的时间来进行身份验证,时间戳通常以30秒为间隔,从而避免反复尝试。
加密算法
SALT值属于随机值。用户注册时,系统用来和用户密码进行组合而生成的随机数值,称作salt值,通称为加盐值。
原理:为用户密码添加Salt值,使得加密的得到的密文更加冷僻,不宜查询。即使黑客有密文查询到的值,也是加了salt值的密码,而非用户设置的密码。salt值是随机生成的一组字符串,可以包括随机的大小写字母、数字、字符,位数可以根据要求而不一样。
HMAC算法是一种基于密钥的报文完整性的验证方法,其安全性是建立在HASH加密算法的基础上的,要求通信双方共享密钥,约定算法,对报文进行Hash运算,形成固定长度的认证码。
通信双方通过认证码的校验来确定报文的合法性,用来加密,数字签名 ,报文验证。(实际的使用中用HMAC做加密也是不可逆加密的,不像用DES/AES这种可逆加密,感觉HMAC和随机盐HASH算法非常像)
总结:HMAC是密钥相关的哈希运算消息认证码(hash-based-Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
HMAC算法是一种执行“校验和”的算法,他通过对数据进行校验来检查数据是否被更改了,在发送数据以前,HMAC算法对数据块和双方约定的公钥进行“散列操作”,以生成称为“摘要”的东西,附加在待发送的数据块中。当数据和摘要到达其目的地时,就使用HMAC算法来生成另一个校验和,如果两个数字相匹配,那么数据未被做任何篡改。否则,就意味着数据在传输或存储过程中被某些居心叵测的人作了手脚。
通过哈希算法,我们可以验证一段数据是否有效,方法就是对比该数据的哈希值,例如,判断用户口令是否正确,我们用保存在数据库中的password_md5对比md5(password)的结果,如果一致,用户输入的口令就是正确的。
为了防止黑客使用彩虹表根据哈希值反推原始口令,在计算哈希的时候,不能值针对原始输入计算,需要增加一个salt来使得相同的输入也能得到不同的哈希。
如果salt是我们自己随机生成的,通常我们计算MD5采用(message + salt),我们把salt看做成一个口令,加salt的哈希:计算一段的message的哈希时,根据不同口令计算出不同的哈希,验证哈希值,必须提供正确的口令。
实际上就是HMAC算法:Keyed-Hashing for Message Authentication
在计算哈希的过程中,把key混入计算过程中
和我们自定义的加的salt算法不同,hmac算法针对所有的哈希算法都是通用的。采用hmac来替代salt算法.
使用场景:该场景就是用来验证文件的安全性
服务端生成key,传给客户端
客户端使用key将账号和密码做HMAC,生成一串散列值,传给服务端
服务端使用key和数据库中用户和密码作为HMAC的散列值,比对来客户端的散列值
HMAC的典型应用:该应用提供了一次安全相应的过程
挑战/响应(“Challenge/Response")身份验证中。
- 先由客户端向服务器发出一个验证请求
- 服务器接到此请求后生成一个随机数并通过网络传输给客户端(此为挑战)
- 客户端将收到的随机数与自己的密钥进行HMAC-SHA1运算并得到一个结果并作为认证的证据传给服务器
- 与此同时,服务器也是用该随机数与储存在服务器数据库中的该客户的密钥进行HMAC-SHA1运算,如果服务器的运算结果与客户端传回的响应结果相同,就认为客户端是一个合法用户
Allocate the operation handle
分配一个进行密码操作句柄,并设置密码的类型和方式
TEE_Result TEE_AllocateOperation(TEE_OperationHandle *operation,
uint32_t algorithm, uint32_t mode,
uint32_t maxKeySize)
分配一个未初始化的临时空间
TEE_Result TEE_AllocateTransientObject(TEE_ObjectType objectType,
uint32_t maxKeySize,
TEE_ObjectHandle *object)
将属性变量填充/赋值到空间变量里
TEE_Result TEE_PopulateTransientObject(TEE_ObjectHandle object,
const TEE_Attribute *attrs,
uint32_t attrCount)
把存放密钥的Object内容保存到句柄中
将保存key得object保存到handle
TEE_Result TEE_SetOperationKey(TEE_OperationHandle operation,
TEE_ObjectHandle key)
分配一个未初始化的临时空间
TEE_Result TEE_AllocateTransientObject(TEE_ObjectType objectType,
uint32_t maxKeySize,
TEE_ObjectHandle *object)
导出key
void TEE_DeriveKey(TEE_OperationHandle operation,
const TEE_Attribute *params, uint32_t paramCount,
TEE_ObjectHandle derivedKey)
TEE_GetObjectBufferAttribute函数的作用是从对象中提取一个缓冲区属性
TEE_Result TEE_GetObjectBufferAttribute(TEE_ObjectHandle object,
uint32_t attributeID, void *buffer,
uint32_t *size)
1、Cryptographic Operations API 加解密函数介绍()
(1)、Generic Operation Functions
TEE_AllocateOperation
TEE_FreeOperation
TEE_SetOperationKey
TEE_SetOperationKey2
(2)、Message Digest Functions 消息摘要
TEE_DigestUpdate
TEE_DigestDoFinal
(3)、Symmetric Cipher Functions 对称加解密
TEE_CipherInit
TEE_CipherUpdate
TEE_CipherDoFinal
(4)、Asymmetric Functions 非对称加解密
TEE_AsymmetricEncrypt, TEE_AsymmetricDecrypt
TEE_AsymmetricSignDigest 和 TEE_AsymmetricVerifyDigest
(4)、Random Data Generation Function 随机数
TEE_GenerateRandom
二.實現
使用密钥和时间戳通过一种算法生成一个6位数字的一次性验证码
实现流程
1、手机下载 Google Authenticator
IOS:在App Store 搜索 Google Authenticator
Android:
打开连接 https://en.softonic.com/download
右上角搜索 Google Authenticator
选择平台 Platform:Android
点击下方 Google Authenticator
点击 FREE DOWNLOAD
再点击 FREE APK DOWNLOAD 等待下载完成
2、生成Secret
调用代码中的 genSecret() 方法生成,记得保存起来,用户需要使用
3、用户扫描二维化进行绑定(也可以拿到密钥手动绑定)
1、当前用户生成的密钥可以生成二维码,用户用app右下角扫描二维码即可添加
参考代码中的main方法
2、手动添加
这里需要知道密钥,添加上名称和密钥即可
4、输入用户名、密码、动态码
5、登录成功
<dependencies>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency> <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
核心代碼分析
- 首先用戶注冊賬號,用戶輸入的注冊賬號和密碼傳入後端
对于原始秘密进行加盐哈希加密原始密码,同时随机生成一个用于谷歌身份验证的密钥。后台数据会保存加密后的密钥,秘密盐值,以及用于谷歌身份验证的密钥。
public String add(User user) {
passwordHelper.encryptPassword(user);//加密密碼
user.setSecret(GoogleAuthenticator.generatrSecretKey());//生成一个随机密钥
this.insert(user);
return GoogleAuthenticator.getQRBarcode(user.getUsername(),user.getSecret());
//生成一个Google身份验证器识别的字符串,把该返回值传回给前台生成二维码
}
public void encryptPassword(USer user) {
try {
PsdProcRlt r;
//对原始密码进行加密处理,处理后得到salt以及密文
r = CryptoApp.PwdProcess(user.getPassword().getBytes());
user.setSalt(Utils.bytesToHexString(r.getSalt()));//设置salt值
user.setPassword(Utils.bytesToHexString(r.getValue()));//设置新的密码
} catch (Exception e)
}
以下代码用于随机生成密钥
public static String generateSecretKey() {
SecureRandom sr = null;
try {
sr = SecrueRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
sr.setSeed(Base64.decodeBase64(SEED));
byte[] buffer = sr.generateSeed(SECRET_SIZE);
Base32 codec = new Base32();
byte[] buffer = codec.encode(buffer);
String encodeKey = new String(bEncodedKey);
return encodeKey;
} catch (NoSuchAlgorithmException e) {
}
return null;
}
//生成一個Google身份验证器识别的字符串,把该方法返回值生成一个二维码扫描
public static String getQRBarcide(String user, String secret) {
String format = "otpauth://totp/%s?secret=%s";
return String.format(format,user,secret);
}
2.客户端和服务器各有一个计数器C,微信小程序二次验证码扫描二维码, 将计数值同步
3.当用户进入登录页,输入账号,密码登录后,系统先对用户输入的密码进行初次验证(双因子认证,第一次认证)
初次驗證
public boolean passwordVerofy(String password,User user) {
boolean match = false;
Object subCredentials = null;
try
{
subCredentials = CryptoApp.PwdTransVlaue(password.getBytes(),PublicUtils.hexStringToBytes(user.getSalt());
}catch(Exception e)
{
e.printStackTrace();
return false;
}
byte[] accountCredentials = Utils.hexStringToBytes(user.getPassword());
match = equals(subCredentials,accountCredentials);
return match;
}
4.初次密码认证通过之后,系统会弹出弹框,要求输入口令,这个口令就是小程序中谷歌身份验证器产生的随机code(双因子认证,第二次认证)
public boolean googleAuthVertify(Integer userid,long code) {
User user = userService.selectById(userid);
long t = System.currentTimeMillis();
GoogleAuthenticator ga = new GoodleAuthenticator();
ga.setWindowSiza(5);
return ga.check_code(user.getSecret(),code,t);
}
//Google Authenticator
// 只从google出了双重身份验证后,就方便了大家,等同于有了google一个级别的安全,但是我们该怎么使用google authenticator (双重身份验证),
//下面是java的算法,这样大家都可以得到根据key得到公共的秘钥了,直接复制,记得导入JAR包:
//
//commons-codec-1.8.jar
//
//junit-4.10.jar
//测试方法:
//
//1、执行测试代码中的“genSecret”方法,将生成一个KEY(用户为testuser),URL打开是一张二维码图片。
//
//2、在手机中下载“GOOGLE身份验证器”。
//
//3、在身份验证器中配置账户,输入账户名(第一步中的用户testuser)、密钥(第一步生成的KEY),选择基于时间。
//
//4、运行authcode方法将key和要测试的验证码带进去(codes,key),就可以知道是不是正确的秘钥了!返回值布尔
//main我就不写了大家~~因为这个可以当做util工具直接调用就行了
//
package coin.util;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
public class GoogleAuthenticator {
// taken from Google pam docs - we probably don't need to mess with these
public static final int SECRET_SIZE = 10;
public static final String SEED = "g8GjEvTbW5oVSV7avLBdwIHqGlUYNzKFI7izOF8GwLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";
public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
int window_size = 3; // default 3 - max 17 (from google docs)最多可偏移的时间
public void setWindowSize(int s) {
if (s >= 1 && s <= 17)
window_size = s;
}
public static Boolean authcode(String codes, String savedSecret) {
// enter the code shown on device. Edit this and run it fast before the
// code expires!
long code = Long.parseLong(codes);
long t = System.currentTimeMillis(); //獲取系統當前時間
GoogleAuthenticator ga = new GoogleAuthenticator();
ga.setWindowSize(15); // should give 5 * 30 seconds of grace...
boolean r = ga.check_code(savedSecret, code, t);
return r;
}
//生成密鑰
public static String genSecret() {
String secret = GoogleAuthenticator.generateSecretKey();
GoogleAuthenticator.getQRBarcodeURL("testuser",
"testhost", secret);
return secret;
}
public static void main(String[] args) {
String format = "otpauth://totp/ACCOUNT?secret=%s&issuer=%s";
String barcodeURL= String.format(format, "PO4F2DIF74VR4ARO", "appname");
//生成二维码
System.out.println(rwm);
}
public static String generateSecretKey() {
SecureRandom sr = null;
//SecureRandom是强随机数生成器,主要应用的场景为:用于安全目的的数据数,例如生成秘钥或者会话标示(session ID),在上文《伪随机数安全性》中,已经给大家揭露了弱随机数生成器的安全问题,而使用SecureRandom这样的强随机数生成器将会极大的降低出问题的风险。
try {
sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
//SHA1PRNG随机数算法,主要是實現這個算法
SecureRandom rng = SecureRandom.getInstance("SHA1PRNG");
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
}
return null;
}
/**
* 获取二维码内容URL
*
* @param user 用户
* @param host 域
* @param secret 密钥
* @return 二维码URL
*/
public static String getQRBarcodeURL(String user, String host, String secret) {
String format = "otpauth://totp/%s?secret=%s&issuer=%s"
return String.format(format, user, host, secret);
}
/**
* 校验code是否正确
*
* @param secret 密钥
* @param code 动态code
* @param timeMsec 时间
* @return
*/
public boolean check_code(String secret, long code, long timeMsec) {
Base32 codec = new Base32();
byte[] decodedKey = codec.decode(secret);
//判斷字符串是否一樣。checkCode
// 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 = -window_size; i <= 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();
throw new RuntimeException(e.getMessage());
//return false;
}
if (hash == code) {
return true;
}
}
// The validation code is invalid.
return false;
}
/**
* 时间校验密钥与code是否匹配
*
* @param key 解密后的密钥
* @param t 时间
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
byte[] data = new byte[8];
long value = t;
//“while(i-->0)表示当 i 的值小于或等于0时,退出while循环
// 如果写成a>>=b,它可以写成a=a>>b,表示将a的二进制值右移b位后再将值赋给a;
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;
}
}
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
/**
* @author alexhu
*
* 主要功能:根据二维码内容生成二维码,并保存在指定位置
*
* 依赖:
* <dependency>
* <groupId>com.google.zxing</groupId>
* <artifactId>core</artifactId>
* <version>3.4.1</version>
* </dependency>
*/
public class GenerateQRCodeUtils {
/**
* 二维码颜色
*/
private static final int BLACK = 0xFF000000;
private static final int WHITE = 0xFFFFFFFF;
/**
* 图片的宽度
*/
private static int WIDTH = 200;
/**
* 图片的高度
*/
private static int HEIGHT = 200;
/**
* 图片的格式
*/
private static String FORMAT = "png";
/**
* 生成二维码
*
* @param basePath 配置文件定义的生成二维码存放文件夹
* @param content 二维码内容
* @return 文件路径
*/
public static String generateQRCodeImg(String basePath, String content){
try {
Map<EncodeHintType, String> encodeMap = new HashMap<EncodeHintType, String>();
// 内容编码,生成二维码矩阵
encodeMap.put(EncodeHintType.CHARACTER_SET, "utf-8");
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, encodeMap);
File file = new File(basePath);
if (!file.exists() && !file.isDirectory()){
file.mkdirs();
}
//文件名,默认为时间为名
String filePath = basePath + System.currentTimeMillis() + "." + FORMAT;
File outputFile = new File(filePath);
if (!outputFile.exists()){
// 生成二维码文件
writeToFile(bitMatrix, FORMAT, outputFile);
}
return filePath;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 把二维码矩阵保存为文件
*
* @param matrix 二维码矩阵
* @param format 文件类型,这里为png
* @param file 文件句柄
* @throws IOException
*/
public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException {
BufferedImage image = toBufferedImage(matrix);
if (!ImageIO.write(image, format, file)) {
throw new IOException("Could not write an image of format " + format + " to " + file);
}
} /**
* 生成二维码矩阵(内存)
*
* @param matrix 二维码矩阵
* @return
*/
public static BufferedImage toBufferedImage(BitMatrix matrix) {
int width = matrix.getWidth();
int height = matrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
}
}
return image;
}
}
import org.junit.Test;
import static org.example.GenerateQRCodeUtils.generateQRCodeImg;
import static org.example.GoogleAuthenticatorUtils.*;
/**
* Unit test for Google Authenticator.
*/
public class GoogleAuthenticatorTest {
/**
* Rigorous Test :-)
*/
@Test
public void genTest() {
/*
* 注意:先运行前两步,获取密钥和二维码url。 然后只运行第三步,填写需要验证的验证码,和第一步生成的密钥
*/
String user = "testUser";
String host = "test.com";
// 第一步:获取密钥
String secret = genSecret(user, host);
System.out.println("secret:" + secret);
// 第二步:根据密钥获取二维码图片url(可忽略)
String url = getQRBarcodeURL(user, host, secret);
System.out.println("url:" + url);
// 第三步 生成二维码
generateQRCodeImg("", url);
}
@Test
public void verifyTest() {
// 第四步:验证(第一个参数是需要验证的验证码,第二个参数是第一步生成的secret运行)
boolean result = authcode("105938", "WUH2RO3Q4D53AF5Z");
System.out.println("result:" + result);
}
}
要验证的验证码,和第一步生成的密钥
*/
String user = “testUser”;
String host = “test.com”;
// 第一步:获取密钥
String secret = genSecret(user, host);
System.out.println(“secret:” + secret);
// 第二步:根据密钥获取二维码图片url(可忽略)
String url = getQRBarcodeURL(user, host, secret);
System.out.println(“url:” + url);
// 第三步 生成二维码
generateQRCodeImg(“”, url);
}@Test
public void verifyTest() {
// 第四步:验证(第一个参数是需要验证的验证码,第二个参数是第一步生成的secret运行)
boolean result = authcode("105938", "WUH2RO3Q4D53AF5Z");
System.out.println("result:" + result);
}}