1. 概述
現(xiàn)在很多小區(qū)都有一些企業(yè)免費(fèi)更換新的門禁、對(duì)講(這個(gè)就是前一、兩年搞的風(fēng)風(fēng)火火的智慧社區(qū)厕倍、社區(qū)O2O等),這些門禁贩疙、對(duì)講有一個(gè)很重要的特色就是可以使用手機(jī)開門讹弯、對(duì)講况既。但有個(gè)問題的是,網(wǎng)絡(luò)不一定穩(wěn)定可靠组民,很多實(shí)施方案使用的還是民用寬帶棒仍,甚至還是一些小的寬帶提供商,穩(wěn)定性可想而知臭胜。
注:網(wǎng)絡(luò)只針對(duì)手機(jī)在線開門而言莫其,一些開門方式不依賴網(wǎng)絡(luò)的
對(duì)于網(wǎng)絡(luò)不穩(wěn)定,我發(fā)現(xiàn)有個(gè)廠商是這樣做的耸三,當(dāng)發(fā)現(xiàn)通過網(wǎng)絡(luò)發(fā)送開門指令失敗乱陡,給出一個(gè)密碼,讓用戶輸入仪壮,從而避免網(wǎng)絡(luò)不良的情況憨颠,無(wú)法開門
實(shí)際上,有些廠商也把這個(gè)功能作為訪客密碼使用积锅,處理方法會(huì)稍微有點(diǎn)差別
很慚愧的是爽彤,具體我沒怎么去體驗(yàn),所以我沒辦法推導(dǎo)具體需求缚陷,但我猜大概需要滿足以下基礎(chǔ)需求:
- 密碼會(huì)自動(dòng)失效适篙,例如兩分鐘內(nèi),不然把這個(gè)密碼分享出去給訪客箫爷,那風(fēng)險(xiǎn)就大了
- 密碼不需要預(yù)先同步到設(shè)備嚷节,如果預(yù)先同步一批密碼到設(shè)備,那個(gè)先用虎锚,那個(gè)后用丹喻,是比較麻煩的事情,而且用戶不一定輸入了該密碼
- 非網(wǎng)絡(luò)驗(yàn)證翁都,畢竟是網(wǎng)絡(luò)通信失敗的時(shí)候使用
實(shí)現(xiàn)
OTP算法
基于以上猜測(cè),我個(gè)人認(rèn)為通過OTP(One Time Password)
實(shí)現(xiàn)起來比較容易谅猾。OTP應(yīng)用很廣泛柄慰,一些網(wǎng)銀的U-KEY令牌就是典型。當(dāng)然也有很多軟件可以支持税娜,諸如:Google Authenticator坐搔、LastPass等。
它的基本原理是:令牌和服務(wù)器存儲(chǔ)相同的秘鑰敬矩,當(dāng)要驗(yàn)證的時(shí)候概行,流程如下:
- 令牌端用當(dāng)前時(shí)間或者遞增計(jì)數(shù)器(或者組合一起),和秘鑰一起進(jìn)行HMAC加密
- 按照一定規(guī)則弧岳,把加密結(jié)果轉(zhuǎn)換成6-8位數(shù)字輸出
- 用戶在網(wǎng)頁(yè)上提交生成的數(shù)字
- 服務(wù)器使用用戶對(duì)應(yīng)的秘鑰按照令牌端的加密流程輸出6-8位數(shù)字
- 比較用戶輸入和服務(wù)端產(chǎn)生凳忙,匹配表示令牌端和服務(wù)器存儲(chǔ)的用戶秘鑰是匹配的业踏,驗(yàn)證通過
如果使用時(shí)間作為計(jì)算輸入,就是TOTP(Time-Based One-Time Password涧卵;如果把遞增計(jì)數(shù)器作為計(jì)算輸入勤家,那么就是HTOP(HMAC-based One-Time Password)
安全性淺析:
- HMAC算法本身的安全性,這里比較多的文章分析
- 秘鑰安全性柳恐,這里涉及比較多方面伐脖,這里列舉幾個(gè)
- 存儲(chǔ)安全性,服務(wù)器認(rèn)為是安全的乐设,畢竟被攻破了讼庇,也沒多大意義;令牌設(shè)計(jì)只能寫入秘鑰(或者一次性寫入)近尚,不能讀取秘鑰蠕啄,并且采用一定的手段把存儲(chǔ)器保護(hù)起來
- 秘鑰長(zhǎng)短,秘鑰長(zhǎng)度不應(yīng)該小于Hash算法長(zhǎng)度肿男,對(duì)于HMAC-SHA1介汹,最小長(zhǎng)度是20字節(jié)(160bit)
- TOTP生命周期,RFC建議在30秒舶沛,實(shí)際也有不少產(chǎn)品是60秒
實(shí)現(xiàn)
算法通常不需要我們實(shí)現(xiàn)嘹承,如果需要Java版本,rfc4226的appendix-C就是一份實(shí)現(xiàn)了如庭。如果需要C/C++版本叹卷,可以使用 google-authenticator;如果需要Python坪它,可以使用:pyotp骤竹。下面是Java實(shí)現(xiàn)(關(guān)鍵的方法:generateOTP我增加中文注釋):
package com.example;
/*
* OneTimePasswordAlgorithm.java
* OATH Initiative,
* HOTP one-time password algorithm
*
*/
/* Copyright (C) 2004, OATH. All rights reserved.
*
* License to copy and use this software is granted provided that it
* is identified as the "OATH HOTP Algorithm" in all material
* mentioning or referencing this software or this function.
*
* License is also granted to make and use derivative works provided
* that such works are identified as
* "derived from OATH HOTP algorithm"
* in all material mentioning or referencing the derived work.
*
* OATH (Open AuTHentication) and its members make no
* representations concerning either the merchantability of this
* software or the suitability of this software for any particular
* purpose.
*
* It is provided "as is" without express or implied warranty
* of any kind and OATH AND ITS MEMBERS EXPRESSaLY DISCLAIMS
* ANY WARRANTY OR LIABILITY OF ANY KIND relating to this software.
*
* These notices must be retained in any copies of any part of this
* documentation and/or software.
*/
import java.io.IOException;
import java.io.File;
import java.io.DataInputStream;
import java.io.FileInputStream ;
import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* This class contains static methods that are used to calculate the
* One-Time Password (OTP) using
* JCE to provide the HMAC-SHA-1.
*
* @author Loren Hart
* @version 1.0
*/
public class OneTimePasswordAlgorithm {
private OneTimePasswordAlgorithm() {}
// These are used to calculate the check-sum digits.
// 0 1 2 3 4 5 6 7 8 9
private static final int[] doubleDigits =
{ 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 };
/**
* Calculates the checksum using the credit card algorithm.
* This algorithm has the advantage that it detects any single
* mistyped digit and any single transposition of
* adjacent digits.
*
* @param num the number to calculate the checksum for
* @param digits number of significant places in the number
*
* @return the checksum of num
*/
public static int calcChecksum(long num, int digits) {
boolean doubleDigit = true;
int total = 0;
while (0 < digits--) {
int digit = (int) (num % 10);
num /= 10;
if (doubleDigit) {
digit = doubleDigits[digit];
}
total += digit;
doubleDigit = !doubleDigit;
}
int result = total % 10;
if (result > 0) {
result = 10 - result;
}
return result;
}
/**
* This method uses the JCE to provide the HMAC-SHA-1
* algorithm.
* HMAC computes a Hashed Message Authentication Code and
* in this case SHA1 is the hash algorithm used.
*
* @param keyBytes the bytes to use for the HMAC-SHA-1 key
* @param text the message or text to be authenticated.
*
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException
* The secret provided was not a valid HMAC-SHA-1 key.
*
*/
public static byte[] hmac_sha1(byte[] keyBytes, byte[] text)
throws NoSuchAlgorithmException, InvalidKeyException
{
// try {
Mac hmacSha1;
try {
hmacSha1 = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException nsae) {
hmacSha1 = Mac.getInstance("HMAC-SHA-1");
}
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmacSha1.init(macKey);
return hmacSha1.doFinal(text);
// } catch (GeneralSecurityException gse) {
// throw new UndeclaredThrowableException(gse);
// }
}
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 an OTP value for the given
* set of parameters.
*
* @param secret the shared secret
* 共享秘鑰,設(shè)備端和服務(wù)器端必須一致往毡,每個(gè)設(shè)備必須不相同
*
* @param movingFactor the counter, time, or other value that
* changes on a per use basis.
* 運(yùn)動(dòng)因子蒙揣,可以是計(jì)數(shù)器,時(shí)間开瞭,或者其他每次變更的值
*
* @param codeDigits the number of digits in the OTP, not
* including the checksum, if any.
* 輸出的OTP密碼位數(shù)
*
* @param addChecksum a flag that indicates if a checksum digit
* should be appended to the OTP.
* 是否添加校驗(yàn)
*
* @param truncationOffset the offset into the MAC result to
* begin truncation. If this value is out of
* the range of 0 ... 15, then dynamic
* truncation will be used.
* Dynamic truncation is when the last 4
* bits of the last byte of the MAC are
* used to determine the start offset.
* 截取偏移懒震,OTP密碼是從HMAC輸出中截取部分轉(zhuǎn)化成10進(jìn)制數(shù)字
* 該參數(shù)表示需要截取的偏移,取值范圍:0 ~ 15嗤详,如果不再這個(gè)范圍內(nèi)个扰,
* 那么使用動(dòng)態(tài)范圍
*
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException
* The secret provided was not
* a valid HMAC-SHA-1 key.
*
* @return A numeric String in base 10 that includes
* {@link codeDigits} digits plus the optional checksum
* digit if requested.
*/
static public String generateOTP(byte[] secret,
long movingFactor,
int codeDigits,
boolean addChecksum,
int truncationOffset)
throws NoSuchAlgorithmException, InvalidKeyException
{
// put movingFactor value into text byte array
String result = null;
int digits = addChecksum ? (codeDigits + 1) : codeDigits;
byte[] text = new byte[8];
for (int i = text.length - 1; i >= 0; i--) {
text[i] = (byte) (movingFactor & 0xff);
movingFactor >>= 8;
}
// compute hmac hash
byte[] hash = hmac_sha1(secret, text);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
if ( (0<=truncationOffset) &&
(truncationOffset<(hash.length-4)) ) {
offset = truncationOffset;
}
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];
if (addChecksum) {
otp = (otp * 10) + calcChecksum(otp, codeDigits);
}
result = Integer.toString(otp);
while (result.length() < digits) {
result = "0" + result;
}
return result;
}
}
使用
該代碼沒什么依賴,下面是使用的例子:
package com.example;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.Scanner;
public class Main {
/**
* OTP秘鑰
*/
private static byte[] otpKey = new byte[80];
/**
* 步長(zhǎng)
*/
private static long STEP = 30;
/**
* 初始化秘鑰葱色,實(shí)際產(chǎn)品當(dāng)中递宅,服務(wù)器端應(yīng)該從數(shù)據(jù)庫(kù)中加載,設(shè)備端從安全存儲(chǔ)區(qū)中加載
*/
static {
for (int i=0; i<otpKey.length; i++) {
otpKey[i] = 0;
}
}
/**
* 根據(jù)指定的時(shí)間產(chǎn)生OTP
*
* @param calendar 指定的時(shí)間
* @param step 步長(zhǎng)
* @return 返回6位數(shù)的OTP密碼
* @throws InvalidKeyException 如果秘鑰無(wú)效,拋出該異常
* @throws NoSuchAlgorithmException 如果系統(tǒng)不支持HMAC-SHA1办龄,拋出該異常
*/
public static String makeCode(final Calendar calendar, long step) throws InvalidKeyException, NoSuchAlgorithmException {
long timeInSeconds = calendar.getTimeInMillis() / 1000L;
timeInSeconds = timeInSeconds / step;
return OneTimePasswordAlgorithm.generateOTP(otpKey, timeInSeconds, 6, false, -1);
}
/**
* 檢查秘鑰
*
* 在實(shí)際項(xiàng)目當(dāng)中烘绽,服務(wù)器和設(shè)備總是存在時(shí)間誤差的,保守起見
* 驗(yàn)證過去土榴,當(dāng)前以及未來30秒的密碼
*
* @param inputCode 用戶輸入的秘鑰
* @param calendar 時(shí)間诀姚,默認(rèn)就是當(dāng)前時(shí)間
* @param step 步長(zhǎng)
* @return 成功返回true
* @throws InvalidKeyException 如果秘鑰無(wú)效,拋出該異常
* @throws NoSuchAlgorithmException 如果系統(tǒng)不支持HMAC-SHA1玷禽,拋出該異常
*/
public static boolean checkCode(String inputCode, final Calendar calendar, long step) throws InvalidKeyException, NoSuchAlgorithmException {
long timeInSeconds = calendar.getTimeInMillis() / 1000L;
timeInSeconds = timeInSeconds / step;
for (int i=-1; i<2; i++) {
String code = OneTimePasswordAlgorithm.generateOTP(
otpKey,
timeInSeconds + i,
6,
false,
-1);
if (code.equals(inputCode)) {
return true;
}
}
return false;
}
public static void main(String[] args) throws Exception {
// 輸出當(dāng)前時(shí)間密碼
Calendar calendar = Calendar.getInstance();
System.out.println(makeCode(calendar, STEP));
System.out.print("輸入:");
Scanner scan = new Scanner(System.in);
String read = scan.nextLine();
System.out.println("輸入:" + read + " 驗(yàn)證:" + checkCode(read, Calendar.getInstance(), STEP));
}
}
運(yùn)行結(jié)果:
在實(shí)際項(xiàng)目中應(yīng)該注意:
- 每個(gè)設(shè)備的秘鑰必須不同赫段,否則同一時(shí)間,全部輸出的秘鑰是一致的
- 秘鑰長(zhǎng)度必須大于20字節(jié)
- 秘鑰的生成矢赁,必須使用安全隨機(jī)數(shù)糯笙,例如:java.security.SecureRandom(注意CVE-2013-7372)、/dev/random(或者 /dev/urandom)撩银、CryptGenRandom (Windows)给涕;然后通過安全通道,同步給設(shè)備额获,或者出廠的階段够庙,通過某種方式燒入。
- 在例子中抄邀,可以重復(fù)使用密碼耘眨,要不可重復(fù)使用,需要自己實(shí)現(xiàn)境肾,這個(gè)看產(chǎn)品需求了剔难。建議可以重復(fù)使用,不然用戶輸入了奥喻,沒來得及開門又閉鎖了偶宫,就麻煩了