智能门禁/对讲的一次性密码开门实现

1. 概述


现在很多小区都有一些企业免费更换新的门禁、对讲(这个就是前一、两年搞的风风火火的智慧社区、社区O2O等),这些门禁、对讲有一个很重要的特色就是可以使用手机开门、对讲。但有个问题的是,网络不一定稳定可靠,很多实施方案使用的还是民用宽带,甚至还是一些小的宽带提供商,稳定性可想而知。

注:网络只针对手机在线开门而言,一些开门方式不依赖网络的

对于网络不稳定,我发现有个厂商是这样做的,当发现通过网络发送开门指令失败,给出一个密码,让用户输入,从而避免网络不良的情况,无法开门

图片.jpg

实际上,有些厂商也把这个功能作为访客密码使用,处理方法会稍微有点差别

很惭愧的是,具体我没怎么去体验,所以我没办法推导具体需求,但我猜大概需要满足以下基础需求:

  • 密码会自动失效,例如两分钟内,不然把这个密码分享出去给访客,那风险就大了
  • 密码不需要预先同步到设备,如果预先同步一批密码到设备,那个先用,那个后用,是比较麻烦的事情,而且用户不一定输入了该密码
  • 非网络验证,毕竟是网络通信失败的时候使用

实现

OTP算法


基于以上猜测,我个人认为通过OTP(One Time Password)实现起来比较容易。OTP应用很广泛,一些网银的U-KEY令牌就是典型。当然也有很多软件可以支持,诸如:Google Authenticator、LastPass等。

中行E令

它的基本原理是:令牌和服务器存储相同的秘钥,当要验证的时候,流程如下:

  1. 令牌端用当前时间或者递增计数器(或者组合一起),和秘钥一起进行HMAC加密
  2. 按照一定规则,把加密结果转换成6-8位数字输出
  3. 用户在网页上提交生成的数字
  4. 服务器使用用户对应的秘钥按照令牌端的加密流程输出6-8位数字
  5. 比较用户输入和服务端产生,匹配表示令牌端和服务器存储的用户秘钥是匹配的,验证通过

如果使用时间作为计算输入,就是TOTP(Time-Based One-Time Password;如果把递增计数器作为计算输入,那么就是HTOP(HMAC-based One-Time Password)

安全性浅析:

  • HMAC算法本身的安全性,这里比较多的文章分析
  • 秘钥安全性,这里涉及比较多方面,这里列举几个
    • 存储安全性,服务器认为是安全的,毕竟被攻破了,也没多大意义;令牌设计只能写入秘钥(或者一次性写入),不能读取秘钥,并且采用一定的手段把存储器保护起来
    • 秘钥长短,秘钥长度不应该小于Hash算法长度,对于HMAC-SHA1,最小长度是20字节(160bit)
  • TOTP生命周期,RFC建议在30秒,实际也有不少产品是60秒

实现


算法通常不需要我们实现,如果需要Java版本,rfc4226的appendix-C就是一份实现了。如果需要C/C++版本,可以使用 google-authenticator;如果需要Python,可以使用:pyotp。下面是Java实现(关键的方法: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
     *                     共享秘钥,设备端和服务器端必须一致,每个设备必须不相同
     *
     * @param movingFactor the counter, time, or other value that
     *                     changes on a per use basis.
     *                     运动因子,可以是计数器,时间,或者其他每次变更的值
     *
     * @param codeDigits   the number of digits in the OTP, not
     *                     including the checksum, if any.
     *                     输出的OTP密码位数
     *
     * @param addChecksum  a flag that indicates if a checksum digit
     *                     should be appended to the OTP.
     *                     是否添加校验
     *
     * @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输出中截取部分转化成10进制数字
     *                     该参数表示需要截取的偏移,取值范围:0 ~ 15,如果不再这个范围内,
     *                     那么使用动态范围
     *
     * @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];

    /**
     * 步长
     */
    private static long STEP = 30;


    /**
     * 初始化秘钥,实际产品当中,服务器端应该从数据库中加载,设备端从安全存储区中加载
     */
    static {
        for (int i=0; i<otpKey.length; i++) {
            otpKey[i] = 0;
        }
    }

    /**
     * 根据指定的时间产生OTP
     *
     * @param calendar 指定的时间
     * @param step 步长
     * @return 返回6位数的OTP密码
     * @throws InvalidKeyException 如果秘钥无效,抛出该异常
     * @throws NoSuchAlgorithmException 如果系统不支持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);
    }

    /**
     * 检查秘钥
     *
     * 在实际项目当中,服务器和设备总是存在时间误差的,保守起见
     * 验证过去,当前以及未来30秒的密码
     *
     * @param inputCode 用户输入的秘钥
     * @param calendar 时间,默认就是当前时间
     * @param step 步长
     * @return 成功返回true
     * @throws InvalidKeyException 如果秘钥无效,抛出该异常
     * @throws NoSuchAlgorithmException 如果系统不支持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 {
        // 输出当前时间密码
        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 + " 验证:" + checkCode(read, Calendar.getInstance(), STEP));
    }
}

运行结果:


image.png

image.png

在实际项目中应该注意:

  1. 每个设备的秘钥必须不同,否则同一时间,全部输出的秘钥是一致的
  2. 秘钥长度必须大于20字节
  3. 秘钥的生成,必须使用安全随机数,例如:java.security.SecureRandom(注意CVE-2013-7372)、/dev/random(或者 /dev/urandom)、CryptGenRandom (Windows);然后通过安全通道,同步给设备,或者出厂的阶段,通过某种方式烧入。
  4. 在例子中,可以重复使用密码,要不可重复使用,需要自己实现,这个看产品需求了。建议可以重复使用,不然用户输入了,没来得及开门又闭锁了,就麻烦了
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容