动态口令设计系列一:基于共享密钥的动态口令方案

动态口令设计系列一:基于共享密钥的动态口令方案2019独角兽企业重金招聘Python工程师标准>>>…

大家好,欢迎来到IT知识分享网。

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

    由于传统的静态口令存在容易泄露(输入型泄密、传输型泄密、共享型泄密、记录型泄密、时间越长泄密危险性越高)且一旦泄露全线崩溃、不能抵御重放攻击的缺点,更安全的动态口令方案应运而生。动态口令被广泛应用于手机支付网银防盗链等场景。基于共享密钥的动态口令方案几乎算得上是最简单的一种动态口令方案,但由于其简单也是很常用的一种方案,当然也有它的局限性。本文结合我在实际工作中的一些心得梳理下这种方案的实现和注意事项。

    一般动态口令的特点有下面这些:

  • 动态性或者时效性,即动态口令产生后经过一段时间就会失效

  • 随机性,动态口令是随机生成的,不可预测

  • 一次性,每个动态口令使用一次后,不可再连续重复使用(这条在某些场景下不一定要满足,某些场景只需要满足第一条的时效性即可。但对于涉及支付、金融、机密信息等场景,这一条必须严格满足)

  • 抗窃听性,由于动态口令的时效性和一次性,即使某个动态口令被窃取了,也由于动态口令的时效性和一次性,而无法被再次使用。这样能避免“重放攻击”

  • 抗暴力猜解,由于动态口令的时效性,要在动态口令有效期内穷暴力猜解出动态口令是比较困难的,再加上可以限制单位时间内猜解出错次数,从而能够更有效地杜绝这种攻击

    为了描述更方便,先抽象一个虚拟的业务场景:

A公司是一家在线旅游公司,网站地址是travel.com,它提供了大量的国内外长短途旅游信息。 为拓展公司业务,A公司决定与B公司(一家国际五星级酒店连锁公司)合作,将自己的一些旅游 相关数据开放给B公司,让B公司的Android客户端和IOS客户端可以直接访问获取A公司的旅游数 据(用户不需要登录即可访问)。技术上,A公司提供两套SDK(Android SDK和IOS SDK)给B 公司,SDK内封装了访问旅游数据HTTP API的业务逻辑和动态口令在客户端的相关逻辑。

    首先注意上面客户端获取旅游数据不需要用户登录,算是一定程度的简化,这样也能更方便地说明动态口令场景。而现实生活中的一些场景一般是需要用户登录的,通常采用静态口令认证与动态口令认证相结合的方案,典型的如两阶段认证(2-Phase Authentication),比如这里列举一个网游服务器登录场景:

  1. 我在登录界面输入用户名和静态口令,点击登录跳转到下一个界面要求我输入短信验证码(即相当于动态口令)

  2. 60s内我将我收到的短信验证码填入,再点击确认,这时才算真正登录成功;如果60s内我还没有输入那个验证码,则验证码自动失效并且登录失败

    借助手机短信或者邮箱发送验证码是动态口令的一种变形方案,它基于的假设是:手机或者邮箱没有丢失,并且即使丢失,静态口令也同时泄露的可能性非常小(当然并不是说可能性非常小就不需要考虑这些极端情况,实际工作中是需要考虑能想到的各种极端情况的,比如为这种极端场景提供手机、邮箱解绑功能或者人工客服协助功能,或为游戏内的贵重道具设置二次口令等)。这种方案可能比较适用于手机支付、登录(比如Gmail登录、微信登录)、修改密码等场景。但它显然不适用于上面的虚拟业务场景。而本文的主角——基于共享密钥的动态口令方案却恰好能很好应对这类业务场景。

    下面是具体流程。

A、共享密钥的离线分发和存储

    “基于共享密钥的动态口令”,顾名思义我们得先生成一个共享密钥,所谓共享密钥指的是服务端和客户端都需要存储这个密钥。共享密钥生成后一般采用离线分发的形式告知客户端接入方,比如工行网银用的U盾里就内置了一个这样的共享密钥。生成共享密钥一般注意这么几点:

  • 共享密钥是随机生成的不可逆字符串,也就是说即使生成算法和共享密钥同时泄露也无法反推出任何有用信息

  • 为Android客户端和IOS客户端各自生成一份共享密钥,避免将所有鸡蛋全放在一个篮子里的问题

    这里提供一个生成共享密钥的伪码,仅供参考(AES Key生成和HMAC-SHA1哈希算法实现可参考:Crypter.java):

/**  * 生成共享密钥  * @param appKey       为B公司分配的应用唯一Key,Android客户端和IOS客户端共享  * @param clientOsType 客户端操作系统类型,1-IOS,2-Android  * @return  */ def generateSharedKey(String appKey, int clientOsType):    String randKey = getAESSecretKey(now()) // 当前服务器时间作为种子,生成随机AES Key作为下面HMAC-SHA1哈希的key    String rawData = appKey + "|" + clientOsType    return hmacSHA1(rawData, randKey) // 进行HMAC-SHA1哈希得到共享密钥

    注意上面提供appKey是出于扩展性考虑,将来A公司可能继续跟其他酒店公司合作,那么可以复用整套动态口令方案,只需要分配不同的appKey,appKey可通过UUID算法生成。将appKey和clientOsType作为哈希运算的数据,可以保证不同appKey、不同的客户端操作系统类型生成的共享密钥是不同的。

    生成共享密钥后,服务端存储很简单,只需要存储appKey、clientOsType到共享密钥的映射关系。

    关键是客户端该如何存储?

    有人可能会选择将共享密钥打包进SDK中,然后将SDK源码混淆。这样做初看起来怪怪的,因为基本上没见过SDK源码还加密的,仔细思考后发现这种做法存在一个明显的大纰漏:如果将共享密钥打包到SDK中,那么未授权的其他公司也可以把这份SDK用于自己的客户端,显然这不是我们想要的结果。

    所以共享密钥只能让B公司自己妥善存储,我们建议B公司将共享密钥加密并编译进客户端程序中。

B、客户端用共享密钥签名各参数并构建动态口令生成请求

    客户端用共享密钥对app_key(客户端自己管理)、client_os_type(SDK API内部获取)等参数并构建动态口令生成请求发送到服务端,签名的目的是防参数被篡改以及防抵赖。签名算法大概如下:

  1. 将参数key-value对按照key的字典排序,并用&连接,比如app_key=xx&client_os_type=xx

  2. 对上面得到的字符串进行Base64编码,即base64Encode(“app_key=xx&client_os_type=xx“)

  3. 以共享密钥为key,用HMAC-SHA1算法对上面得到的Base64编码字符串进行签名得到签名参数sig,即:

sig = hmacSHA1(base64Encode("app_key=xx&client_os_type=xx"), sharedKey);

    然后将参数连同签名一起发往服务端请求生成动态口令:

curl -X POST -d "app_key=xx&client_os_type=xx&sig=xx" http://open.travel.com/otp

C、服务端校验参数签名并生成动态口令

    服务端接收到上面的请求后,简单进行参数是否为空等有效性校验后提取app_key、client_os_type参数,然后根据A步骤存储的映射关系得到服务端存储的共享密钥,然后按照与B步骤相同的方法对参数进行签名,并与传进来的sig参数比对:如果不匹配则返回错误;如果匹配则说明客户端是已授权的客户端,则继续生成动态口令,生成动态口令的伪码如下:

/**  * 服务端生成动态口令  */ randKey = getAESSecretKey(now()) // 以当前服务器时间作为种子得到随机AES Key rawData = "app_key:" + appKey + "|client_os_type:" + clientOsType + "|" + randKey otp = hmacSHA1(rawData, sharedKey) // sharedKey是服务端存储的共享密钥 /**  * 服务端存储动态口令并返回给客户端  */ save(otp, 600) // 存储动态口令,并设置600s的失效时间 return otp

注意:

  • randKey是为了增加随机性,这样即使动态口令生成算法被公开,也无所谓

  • 上面共享密钥参与了动态口令的生成过程,这并不是必须的,如果认为共享密钥参与动态口令生成会增大共享密钥被激活成功教程的风险,那么可以自行设计,只要保证算法具有足够的随机性不可逆性就好

D、客户端使用动态口令请求数据

    客户端接收到动态口令后,就可以使用动态口令请求旅游数据了。一样的用shared_key对参数(包括动态口令otp参数)进行数字签名,签名算法也跟B步骤一样:

curl -X POST -d "app_key=xx&client_os_type=xx&otp=xx&q=海南&sig=xx" 
   http://open.travel.com/hotline

    上面q参数是具体数据API调用参数,即请求带有“海南”关键词的热门旅游路线数据。

E、服务端先验证参数签名,再验证动态口令有效性

    服务端收到D步骤发来的请求后,首先对参数进行签名验证,跟C步骤差不多。然后验证动态口令是否有效:如果无效则提示错误;如果有效(则让动态口令失效,这里可以不立即失效,但对于支付等场景必须立即失效),并继续调用开放数据API,并返回数据给客户端。

    这里可以增加动态口令出错锁定机制:即在某一段时间内动态口令尝试出错次数达到某个值,就锁定相应客户端(如根据IP地址),一段时间接收但不处理那个客户端的请求。

    总体来说,共享密钥被用于数字签名、身份认证。期间还尝试过由客户端生成动态口令,而服务端只进行动态口令验证,但仔细推敲后,认为那种方案不利于服务端进行监控管理并可能给动态口令的时效性带来破坏(比如基于客户端系统时间生成动态口令,那么客户端可以修改系统时间来让动态口令永远不会过期)。

    而网银USB Key,一般是将共享密钥内置在硬件中,并且每个USB Key的共享密钥都不一样,所以网银的动态口令可以在客户端生成,然后在服务端验证。我们应该具体业务场景具体分析。

    最后记住一点,基于共享密钥的动态口令方案的核心安全前提是:

共享密钥能够被客户端妥善安全存储

    如果不满足这个前提,那么就不能使用这种动态口令方案。    

转载于:https://my.oschina.net/feichexia/blog/305850

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/11253.html

(0)

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信