文章目录
工作原理
- TOTP算法(Time-based One-time Password algorithm)是一种从共享密钥和当前时间计算一次性密码的算法。 它已被采纳为Internet工程任务组标准RFC 6238,是Initiative for Open Authentication(OATH)的基石,并被用于许多双因素身份验证系统。
- TOTP是基于散列的消息认证码(HMAC)的示例。 它使用加密哈希函数将密钥与当前时间戳组合在一起以生成一次性密码。 由于网络延迟和不同步时钟可能导致密码接收者必须尝试一系列可能的时间来进行身份验证,因此时间戳通常以30秒的间隔增加,从而减少了潜在的搜索空间。
1, 服务端安装配置
方式1, yum原安装
yum install epel-release
yum install pam-devel qrencode-libs google-authenticator
方式2, 编译安装
Google Authenticator 项目地址:https://github.com/google/google-authenticator-libpam
[root@test-c6 google-authenticator-libpam-1.09]#
./bootstrap.sh
./configure
make
make install
[root@test-c6 google-authenticator-libpam-1.09]# ls /usr/local/lib/security/
pam_google_authenticator.la pam_google_authenticator.so
[root@test-c6 google-authenticator-libpam-1.09]# cp /usr/local/lib/security/*.so /lib64/security/
[root@test-c6 google-authenticator-libpam-1.09]# ls google-authenticator
google-authenticator
配置用户随机密码
运行google-authenticator二进制文件以在您的主目录中创建一个新的密钥
-
如果不显示二维码是因为没有安装依赖包qrencode-libs
,也可用打印的url地址也可以获取二维码,或者在Android“ Google Authenticator”输入字母数字密钥 -
yum install qrencode-libs
https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/root@test-c6%3Fsecret%3D3GM6753RARIDV2I27BQ4CZBGBQ%26issuer%3Dtest-c6
#默认每个用户使用自己家目录的密码文件:~/.google_authenticator, 这样如果有用户没有生成该文件就无法登录
# 此时可以添加参数:设置所有用户登录使用指定的用户名,并使用同一个密码文件: secret=... user=...
# mv /root/.google_authenticator /var/lib/
# sed -i '2iauth required pam_google_authenticator.so secret=/var/lib/.google_authenticator user=root' /etc/pam.d/sshd
sed -i '2iauth required pam_google_authenticator.so' /etc/pam.d/sshd
sed -i 's/^ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config
#若sshd服务是编译安装的,需要启用PAM支持: ./configure --with-pam
# 并且启用PAM: sed -i 's/^UsePAM no/UsePAM yes/' /usr/local/openss/etc/sshd_config
service sshd restart
2, 手机客户端安装,并测试登录
- 1, App 扫描二维码,获取随机验证码
- 2, 如果密码一直提示输入验证码,可能是:服务器和手机,时间不一致,需要同步时间
app下载:github安装包 https://github.com/google/google-authenticator/tree/master/mobile
测试登录:
服务器上: 终端测试ssh登录
[root@test-c6 ~]# ssh localhost
Verification code:
Password:
Last login: Wed Mar 10 14:34:33 2021 from 192.168.56.1
[root@test-c6 ~]# logout
Connection to localhost closed.
## 等待几秒,手机刷出新的验证码后,切换登录用户来登录(同一个验证码只能用一次)
[root@test-c6 ~]# ssh vagrant@localhost
Verification code:
Password:
Last login: Wed Mar 10 14:34:54 2021 from ::1
[vagrant@test-c6 ~]$
客户机上: 测试ssh登录
xshell登录: 保存<用户名> <密码>, 勾选 < Keyboard Interactive>
3, java 实现TOTP
- 第一步:服务端根据选定的密钥生成二维码,并计算动态口令
- 第二步:客户端app扫描二维码,获取动态口令来登录
package com.example.demo.googleauth;
import java.io.*;
import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class GoogleAuthTest {
public static void main(String[] args) throws IOException, WriterException {
//String key = getRandomSecretKey();
String key = "45fo3qwmfbrpgq6lora4emdxwyvlrwu";
String totpCode = getTOTPCode(key);
System.out.println("SecretKey==="+key +" pass=== "+totpCode);
//SecretKey===345fo3qwmfbrpgq6lora4emdxwyvlrwu,, pass=== 846242
String imgContent = GoogleAuthTest.getGoogleAuthenticatorBarCode(key, "admin", "website verify code");
//打印二维码在线生成地址
System.out.println("url=== https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl="+imgContent);
//输出二维码(图片宽高=200)
createQRCode(imgContent,new FileOutputStream("D:/a.png"),200,200);
}
/**
* 生成随机的密钥
* @return
*/
public static String getRandomSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
return base32.encodeToString(bytes).toLowerCase();
}
/**
* 根据密钥,生成 TOPT 密钥的 URI 字符串
* @param secretKey
* @param account
* @param issuer
* @return
*/
public static String getGoogleAuthenticatorBarCode(String secretKey, String account, String issuer) {
try {
return "otpauth://totp/"
+ URLEncoder.encode(issuer + ":" + account, "UTF-8").replace("+", "%20")
+ "?secret=" + URLEncoder.encode(secretKey, "UTF-8").replace("+", "%20")
+ "&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
/**
* 根据 TOPT 密钥的 URI 字符串 生成二维码
* @param barCode
* @param outputStream
* @param height
* @param width
* @throws WriterException
* @throws IOException
*/
public static void createQRCode(String barCode, OutputStream outputStream, int height, int width) throws WriterException, IOException {
BitMatrix matrix = new MultiFormatWriter().encode(barCode, BarcodeFormat.QR_CODE, width, height);
MatrixToImageWriter.writeToStream(matrix, "png", outputStream);
}
/// TOTP //
/**
* 根据密钥,计算出当前时间的动态口令 (30s会变化一次)
* @param secretKey
* @return
*/
public static String getTOTPCode(String secretKey) {
Base32 base32 = new Base32();
byte[] bytes = base32.decode(secretKey);
String hexKey = Hex.encodeHexString(bytes);
long time = (System.currentTimeMillis() / 1000) / 30;
String hexTime = Long.toHexString(time);
//return generateTOTP(key, time, returnDigits, "HmacSHA256");
//generateTOTP(key, time, returnDigits, "HmacSHA512");
//return generateTOTP(key, time, returnDigits, "HmacSHA1");
return generateTOTP(hexKey, hexTime, "6", "HmacSHA1");
}
/**
* This method uses the JCE to provide the crypto algorithm.
* HMAC computes a Hashed Message Authentication Code with the
* crypto hash algorithm as a parameter.
*
* @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
* HmacSHA512)
* @param keyBytes: the bytes to use for the HMAC key
* @param text: the message or text to be authenticated
*/
private static byte[] hmac_sha(String crypto, byte[] keyBytes,
byte[] text){
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
}
/**
* This method converts a HEX string to Byte[]
* @param hex: the HEX string
* @return: a byte array
*/
private static byte[] hexStr2Bytes(String hex){
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
byte[] bArray = new BigInteger("10" + hex,16).toByteArray();
// Copy all the REAL bytes, not the "first"
byte[] ret = new byte[bArray.length - 1];
for (int i = 0; i < ret.length; i++)
ret[i] = bArray[i+1];
return ret;
}
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {
1,10,100,1000,10000,100000,1000000,10000000,100000000 };
/**
* This method generates a TOTP value for the given
* set of parameters.
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @param crypto: the crypto function to use
*
* @return: a numeric String in base 10 that includes digits
*/
public static String generateTOTP(String key,
String time,
String returnDigits,
String crypto){
int codeDigits = Integer.decode(returnDigits).intValue();
String result = null;
// Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
while (time.length() < 16 )
time = "0" + time;
// Get the HEX in a Byte[]
byte[] msg = hexStr2Bytes(time);
byte[] k = hexStr2Bytes(key);
byte[] hash = hmac_sha(crypto, k, msg);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
int binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[codeDigits];
result = Integer.toString(otp);
while (result.length() < codeDigits) {
result = "0" + result;
}
return result;
}
}