U盾是一个咋看起来很神奇的东西,后来有了软件版,比如 QQ 邮箱有微信密令,Google 也有 Google Authenticator。今天 Google 了实现原理,并且实现了一个简单版本。

核心是 Hmac,服务器颁发给客户端一个 key(字符串)。服务器和客户端都知道这个字符串,并且服务器保存这个 key 颁发给哪个设备(用户)了。验证的时候,服务器和客户端同时用相同的算法 hash 某个相同的东西,然后客户端把这个 hash 发给服务器,因为 key、算法、加密内容都相同,所以服务器很好验证。

那么加密哪个内容呢?当然是时间。所以必须保证服务器和客户端的时间是相同的,离线版的硬件的话,依靠时钟;在线版的软件客户端可以定时去同步服务器端的时间(知道 diff 就可以了)。

如何保证密码的有效时间?因为输入的时候肯定是有时间差的,如果很快就过期了,那无论如何也输入不对了。

时间可以按照分钟算,不要按照秒(因为秒太快了)。比如,加密内容是,现在距离 1970-01-01 过了多少分钟了。这样有效期就相对长了,足够用户输入了。


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

public class OneTimePassword {
	private double timeDiff;
	private byte[] key;
	private final int ttl = 30;
	private final String algorithm = "HmacSHA1";
	private final int[] lengthBase = new int[] { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };
	private Mac mac;

	public OneTimePassword(double timeDiff, String key) {
		super();
		this.timeDiff = timeDiff;
		this.key = hexStringToBytes(key);
		// 初始化 hmac 算法
		try {
			SecretKeySpec signingKey = new SecretKeySpec(this.key, this.algorithm);
			this.mac = Mac.getInstance(this.algorithm);
			mac.init(signingKey);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public OneTimePassword(String key) {
		this(0, key);
	}

	private byte[] longToBytes(long l) {
		byte[] result = new byte[8];
		for (int i = 7; i >= 0; i--) {
			result[i] = (byte) (l & 0xFF);
			l >>= 8;
		}
		return result;
	}

	public byte[] hexStringToBytes(String hexString) {
		if (hexString == null || hexString.equals("")) {
			return null;
		}
		hexString = hexString.toUpperCase();
		int length = hexString.length() / 2;
		char[] hexChars = hexString.toCharArray();
		byte[] d = new byte[length];
		for (int i = 0; i < length; i++) {
			int pos = i * 2;
			d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
		}
		return d;
	}

	private byte charToByte(char c) {
		return (byte) "0123456789ABCDEF".indexOf(c);
	}

	private byte[] count() {
		long count = (long) ((System.currentTimeMillis() + this.timeDiff) / (this.ttl * 1000));
		return longToBytes(count);
	}

	public String password(int passwordLength) {
		if (passwordLength > lengthBase.length)
			passwordLength = lengthBase.length - 1;
		if (passwordLength < 0)
			passwordLength = 0;
		byte[] result = mac.doFinal(this.count());

		int offset = result[result.length - 1] & 0xf;
		long value = ((result[offset] & 0x7f) << 24) | ((result[offset + 1] & 0xff) << 16)
				| ((result[offset + 2] & 0xff) << 8) | (result[offset + 3] & 0xff);

		return String.valueOf(value % lengthBase[passwordLength]);
	}

	public String password() {
		return password(6);
	}

	public static void main(String[] args) {
		OneTimePassword otp = new OneTimePassword("e959af1d9a80782d1dc2fce35968dca0d7d130cd");
		System.out.println(otp.password());
	}

}