智能門禁/對(duì)講的一次性密碼開門實(shí)現(xiàn)

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ú)法開門

圖片.jpg

實(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等。

中行E令

它的基本原理是:令牌和服務(wù)器存儲(chǔ)相同的秘鑰敬矩,當(dāng)要驗(yàn)證的時(shí)候概行,流程如下:

  1. 令牌端用當(dāng)前時(shí)間或者遞增計(jì)數(shù)器(或者組合一起),和秘鑰一起進(jìn)行HMAC加密
  2. 按照一定規(guī)則弧岳,把加密結(jié)果轉(zhuǎn)換成6-8位數(shù)字輸出
  3. 用戶在網(wǎng)頁(yè)上提交生成的數(shù)字
  4. 服務(wù)器使用用戶對(duì)應(yīng)的秘鑰按照令牌端的加密流程輸出6-8位數(shù)字
  5. 比較用戶輸入和服務(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é)果:


image.png

image.png

在實(shí)際項(xiàng)目中應(yīng)該注意:

  1. 每個(gè)設(shè)備的秘鑰必須不同赫段,否則同一時(shí)間,全部輸出的秘鑰是一致的
  2. 秘鑰長(zhǎng)度必須大于20字節(jié)
  3. 秘鑰的生成矢赁,必須使用安全隨機(jī)數(shù)糯笙,例如:java.security.SecureRandom(注意CVE-2013-7372)、/dev/random(或者 /dev/urandom)撩银、CryptGenRandom (Windows)给涕;然后通過安全通道,同步給設(shè)備额获,或者出廠的階段够庙,通過某種方式燒入。
  4. 在例子中抄邀,可以重復(fù)使用密碼耘眨,要不可重復(fù)使用,需要自己實(shí)現(xiàn)境肾,這個(gè)看產(chǎn)品需求了剔难。建議可以重復(fù)使用,不然用戶輸入了奥喻,沒來得及開門又閉鎖了偶宫,就麻煩了
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市环鲤,隨后出現(xiàn)的幾起案子纯趋,更是在濱河造成了極大的恐慌,老刑警劉巖冷离,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件结闸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡酒朵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門扎附,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔫耽,“玉大人,你說我怎么就攤上這事〕渍。” “怎么了图甜?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鳖眼。 經(jīng)常有香客問我黑毅,道長(zhǎng),這世上最難降的妖魔是什么钦讳? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任矿瘦,我火速辦了婚禮,結(jié)果婚禮上愿卒,老公的妹妹穿的比我還像新娘缚去。我一直安慰自己,他們只是感情好琼开,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布易结。 她就那樣靜靜地躺著,像睡著了一般柜候。 火紅的嫁衣襯著肌膚如雪搞动。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天渣刷,我揣著相機(jī)與錄音鹦肿,去河邊找鬼。 笑死飞主,一個(gè)胖子當(dāng)著我的面吹牛狮惜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碌识,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼碾篡,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了筏餐?” 一聲冷哼從身側(cè)響起开泽,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎魁瞪,沒想到半個(gè)月后穆律,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡导俘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年峦耘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旅薄。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辅髓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情洛口,我是刑警寧澤矫付,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站第焰,受9級(jí)特大地震影響买优,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜挺举,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一杀赢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧豹悬,春花似錦葵陵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至伤柄,卻和暖如春绊困,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背适刀。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工秤朗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人笔喉。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓取视,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親常挚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子作谭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容