暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

技术杂谈丨基于时间的二次认证 TOTP

沃趣技术 2024-09-24
813
01
背景


系统登录,一般为用户名、密码认证,或者增加验证码与认证锁定,避免暴力破解。


为了更加安全,可以增加动态一次性口令认证。比如手机验证码、u盾。但这些对接成本比较高,Google推出了TOTP,简单安全,并能在脱机环境使用。


上验证图,验证码每30秒更新一次,很高大上:



02
算法原理


TOTP全称为:Time-Based One-Time Password Algorithm,是基于HOTP(HMAC-Based One-Time Password Algorithm)实现的,先大致介绍下HOTP的原理。


两个算法的说明文档可参考:

  • HOTP:https://datatracker.ietf.org/doc/html/rfc4226

  • TOTP:https://datatracker.ietf.org/doc/html/rfc6238


HOTP的算法原理,参考官方文档,公式为:

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))


其中,HMAC-SHA-1可同样换成SHA256,SHA512,MD5, 当前使用SHA25的安全性是足够的。


HMAC为标准的算法,不做过多展开,但Truncate为一种特殊的算法,目的是为了将hmac计算后的20位密文转换成方便使用的6位或8位数字,方便输入。


go有相关的开源代码实现,可参考:github.com/pquerna/otp


算法流程


  1. 取hmac后的20位密文最后一位,按16取余,得到0-15的数字,作为偏移量

  2. 从偏移量开始,取4个字符(每个字符取1个字节),拼接成一个数字(64位系统中,一个int占用4个字节),同时最高位设置成0,避免结果为负数

  3. 基于得到的数字取余。如果是需要6位,则基于1000000取余,8位同理


通过上面算法,基于密钥K,次数C,就算出来了动态验证码。而密钥通过其它方式同步,类似aksk传递方式。而次数C,每次使用后,就会增加。


TOTP的变化


由于次数C的维护比较麻烦,客户端与服务端使用后加1,而如果出现不同步,就需要人工干预,比较麻烦。


针对这个问题,TOTP直接使用unix时间作为C,通过这个网站可以进行时间互换:https://www.unixtimestamp.com/



而由于时间是一直变化的,所以不能单纯的用时间,需要将时间进行转换:C/30 默认是除以30秒,表示动态验证码有效期为30秒 而在验证算法中,服务端有个参数可以控制允许上下浮动区间,如果配置1,则可以上下浮动30秒,有效期就变成了90秒。



03
算法验证


先实现一个服务端,用于生成密钥,服务端运行,生成密钥与二维码,等待输入验证:



客户端下载google认证app,并扫码导入,可看到验证码每30秒刷新一次。app不允许截图,拍照有点模糊:



chrome也有相关插件:



服务端验证客户端验码,认证通过。由于我服务端默认开启了偏差1,所以有效期为90秒。



服务端参考代码如下:


package main

import (
 "bufio"
 "bytes"
 "fmt"
 "image/png"
 "log"
 "os"

 "github.com/pquerna/otp"
 "github.com/pquerna/otp/totp"
)

func main() {
 k, err := totp.Generate(totp.GenerateOpts{
  Issuer:      "test.com",
  AccountName: "testUser",
  Period:      30,
  SecretSize:  32,
  Digits:      otp.DigitsSix,
  Algorithm:   otp.AlgorithmSHA256,
 })
 if err != nil {
  log.Fatal("gen key failed. ", err)
 }
 log.Println("gen TOTP: ", k.String())

 var buf bytes.Buffer
 img, err := k.Image(200, 200)
 if err != nil {
  log.Fatal("gen image failed ", err)
 }
 err = png.Encode(&buf, img)
 if err != nil {
  log.Fatal("gen png failed ", err)
 }
 err = os.WriteFile("qr-code.png", buf.Bytes(), 0644)
 if err != nil {
  log.Fatal("write png failed ", err)
 }
 fmt.Println("qf code written to qf-code.png. ")

 fmt.Println("Validating TOTP...")
 for {
  passcode := promptForPasscode()
  valid := totp.Validate(passcode, k.Secret())
  if valid {
   println("Valid passcode!")
   continue
  } else {
   println("Invalid passcode!", passcode)
   continue
  }
 }
}

func promptForPasscode() string {
 reader := bufio.NewReader(os.Stdin)
 fmt.Print("Enter Passcode: ")
 text, _ := reader.ReadString('\n')
 return text[:len(text)-1] // remove last \n
}



本期作者丨沃趣科技产品研发部

版权作品,未经许可禁止转载




往期作品快速浏览:



文章转载自沃趣技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论