diff --git a/assets/buildinfo.txt b/assets/buildinfo.txt
index a60e19eb..33afc321 100644
--- a/assets/buildinfo.txt
+++ b/assets/buildinfo.txt
@@ -1,8 +1,8 @@
-BuildVersion=latest v8.0.7 2024-01-29 22:37:58
+BuildVersion=latest v8.0.7 2024-01-30 01:51:10
ReleaseVersion=v8.0.7
-BuildTime=2024-01-29 22:37:58
+BuildTime=2024-01-30 01:51:10
BuildName=toughradius
-CommitID=5b4fef8be34d0ebed5df9fbda038f007c39f2571
-CommitDate=Mon, 29 Jan 2024 22:29:31 +0800
+CommitID=45e1ccc7009f22948a6c264152869c28e556a7fa
+CommitDate=Tue, 30 Jan 2024 01:50:54 +0800
CommitUser=jamiesun.net@gmail.com
-CommitSubject=2024-01-29 22:29:23 : ver
+CommitSubject=Add support for EAP-OTP authentication method
diff --git a/assets/static/views/settings.js b/assets/static/views/settings.js
index 2cc6b2d9..1b642f1b 100644
--- a/assets/static/views/settings.js
+++ b/assets/static/views/settings.js
@@ -13,7 +13,6 @@ settingsUi.getConfigView = function (citem) {
return {id: "settings_form_view"}
}
-
settingsUi.getSystemConfigView = function (citem) {
let formid = webix.uid().toString();
return {
@@ -109,12 +108,15 @@ settingsUi.getRadiusConfigView = function (citem) {
},
{
view: "radio", name: "RadiusEapMethod", labelPosition: "top", label: tr("settings", "EAP certification methodology"),
- options: ["noeap","eap-md5", "eap-mschapv2"],
+ options: ["noeap","eap-md5", "eap-mschapv2", "eap-otp"],
bottomLabel: tr("settings", "eap certification methodology")
},
{
view: "radio", name: "RadiusIgnorePwd", labelPosition: "top", label: tr("settings", "Ignore Passowrd check"),
- options: [{id: 'enabled', value: gtr("Yes")}, {id: 'disabled', value: gtr("No")}],
+ options: [
+ {id: 'enabled', value: gtr("Yes")},
+ {id: 'disabled', value: gtr("No")}
+ ],
bottomLabel: tr("settings", "Password authentication is ignored, but does not apply to MsChapv2 authentication mode.")
},
{}
diff --git a/assets/static/views/settings.min.js b/assets/static/views/settings.min.js
index cd5bfbc2..77442a39 100644
--- a/assets/static/views/settings.min.js
+++ b/assets/static/views/settings.min.js
@@ -4,7 +4,7 @@ elementsConfig:{labelWidth:180,labelPosition:"left"},url:"/admin/settings/system
"Login form title")},{}]}]}};
settingsUi.getRadiusConfigView=function(a){let c=webix.uid().toString();return{id:"settings_form_view",rows:[{padding:2,cols:[{view:"label",label:" "+a.title,css:"dash-title-b",width:240,align:"left"},{},wxui.getPrimaryButton(gtr("Save"),150,!1,function(){let d=$$(c).getValues();d.ctype="radius";webix.ajax().post("/admin/settings/update",d).then(function(b){b=b.json();webix.message({type:b.msgtype,text:b.msg,expire:3E3})})})]},{id:c,view:"form",scroll:!0,paddingX:10,paddingY:10,
elementsConfig:{labelWidth:180,labelPosition:"left"},url:"/admin/settings/radius/query",elements:[{view:"counter",name:"AcctInterimInterval",labelPosition:"top",label:tr("settings","Default Acctounting interim interval"),bottomLabel:tr("settings","Default Acctounting interim interval, Recommended 120-600 seconds")},{view:"counter",name:"AccountingHistoryDays",labelPosition:"top",label:tr("global","Radius accounting logging expire days"),bottomLabel:tr("settings","Radius logging expire days, set according to the disk size. ")},
-{view:"radio",name:"RadiusEapMethod",labelPosition:"top",label:tr("settings","EAP certification methodology"),options:["noeap","eap-md5","eap-mschapv2"],bottomLabel:tr("settings","eap certification methodology")},{view:"radio",name:"RadiusIgnorePwd",labelPosition:"top",label:tr("settings","Ignore Passowrd check"),options:[{id:"enabled",value:gtr("Yes")},{id:"disabled",value:gtr("No")}],bottomLabel:tr("settings","Password authentication is ignored, but does not apply to MsChapv2 authentication mode.")},
+{view:"radio",name:"RadiusEapMethod",labelPosition:"top",label:tr("settings","EAP certification methodology"),options:["noeap","eap-md5","eap-mschapv2","eap-otp"],bottomLabel:tr("settings","eap certification methodology")},{view:"radio",name:"RadiusIgnorePwd",labelPosition:"top",label:tr("settings","Ignore Passowrd check"),options:[{id:"enabled",value:gtr("Yes")},{id:"disabled",value:gtr("No")}],bottomLabel:tr("settings","Password authentication is ignored, but does not apply to MsChapv2 authentication mode.")},
{}]}]}};
settingsUi.getTr069ConfigView=function(a){let c=webix.uid().toString();return{id:"settings_form_view",rows:[{padding:2,cols:[{view:"label",label:" "+a.title,css:"dash-title-b",width:240,align:"left"},{},wxui.getPrimaryButton(gtr("Save"),150,!1,function(){let d=$$(c).getValues();d.ctype="tr069";webix.ajax().post("/admin/settings/update",d).then(function(b){b=b.json();webix.message({type:b.msgtype,text:b.msg,expire:3E3})})})]},{id:c,view:"form",scroll:!0,paddingX:10,paddingY:10,
elementsConfig:{labelWidth:180,labelPosition:"left"},url:"/admin/settings/tr069/query",elements:[{view:"radio",name:"CpeAutoRegister",labelPosition:"top",label:tr("settings","Cpe auto register"),options:["enabled","disabled"],bottomLabel:tr("settings","Automatic registration of new CPE devices")},{view:"text",name:"TR069AccessAddress",labelPosition:"top",label:tr("settings","TR069 access address"),bottomLabel:tr("settings","Toughradius TR069 access address, HTTP | https://domain:port")},{view:"text",
diff --git a/common/totp/totp.go b/common/totp/totp.go
new file mode 100644
index 00000000..d8097319
--- /dev/null
+++ b/common/totp/totp.go
@@ -0,0 +1,109 @@
+package totp
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/base32"
+ "encoding/binary"
+ "fmt"
+ "net/url"
+ "strings"
+ "time"
+)
+
+type GoogleAuth struct {
+}
+
+func NewGoogleAuth() *GoogleAuth {
+ return &GoogleAuth{}
+}
+
+func (ga *GoogleAuth) un() int64 {
+ return time.Now().UnixNano() / 1000 / 30
+}
+
+func (ga *GoogleAuth) hmacSha1(key, data []byte) []byte {
+ h := hmac.New(sha1.New, key)
+ if total := len(data); total > 0 {
+ h.Write(data)
+ }
+ return h.Sum(nil)
+}
+
+func (ga *GoogleAuth) base32encode(src []byte) string {
+ return base32.StdEncoding.EncodeToString(src)
+}
+
+func (ga *GoogleAuth) base32decode(s string) ([]byte, error) {
+ return base32.StdEncoding.DecodeString(s)
+}
+
+func (ga *GoogleAuth) toBytes(value int64) []byte {
+ var result []byte
+ mask := int64(0xFF)
+ shifts := [8]uint16{56, 48, 40, 32, 24, 16, 8, 0}
+ for _, shift := range shifts {
+ result = append(result, byte((value>>shift)&mask))
+ }
+ return result
+}
+
+func (ga *GoogleAuth) toUint32(bts []byte) uint32 {
+ return (uint32(bts[0]) << 24) + (uint32(bts[1]) << 16) +
+ (uint32(bts[2]) << 8) + uint32(bts[3])
+}
+
+func (ga *GoogleAuth) oneTimePassword(key []byte, data []byte) uint32 {
+ hash := ga.hmacSha1(key, data)
+ offset := hash[len(hash)-1] & 0x0F
+ hashParts := hash[offset : offset+4]
+ hashParts[0] = hashParts[0] & 0x7F
+ number := ga.toUint32(hashParts)
+ return number % 1000000
+}
+
+// 获取秘钥
+func (ga *GoogleAuth) GetSecret() string {
+ var buf bytes.Buffer
+ binary.Write(&buf, binary.BigEndian, ga.un())
+ return strings.ToUpper(ga.base32encode(ga.hmacSha1(buf.Bytes(), nil)))
+}
+
+// 获取动态码
+func (ga *GoogleAuth) GetCode(secret string) (string, error) {
+ secretUpper := strings.ToUpper(secret)
+ secretKey, err := ga.base32decode(secretUpper)
+ if err != nil {
+ return "", err
+ }
+ number := ga.oneTimePassword(secretKey, ga.toBytes(time.Now().Unix()/30))
+ return fmt.Sprintf("%06d", number), nil
+}
+
+// 获取动态码二维码内容
+func (ga *GoogleAuth) GetQrcode(user, secret, stype string) string {
+ return fmt.Sprintf("otpauth://totp/%s:%s?issuer=%s&secret=%s", stype, user, stype, secret)
+}
+
+// 获取动态码二维码图片地址,这里是第三方二维码api
+func (ga *GoogleAuth) GetQrcodeUrl(user, secret, stype string) string {
+ qrcode := ga.GetQrcode(user, secret, stype)
+ width := "200"
+ height := "200"
+ data := url.Values{}
+ data.Set("data", qrcode)
+ return "https://api.qrserver.com/v1/create-qr-code/?" + data.Encode() + "&size=" + width + "x" + height + "&ecc=M";
+}
+
+// 验证动态码
+func (ga *GoogleAuth) VerifyCode(secret, code string) (bool, error) {
+ _code, err := ga.GetCode(secret)
+ fmt.Println(_code, code, err)
+ if err != nil {
+ return false, err
+ }
+ return _code == code, nil
+}
+
+
diff --git a/common/totp/totp_test.go b/common/totp/totp_test.go
new file mode 100644
index 00000000..04c63825
--- /dev/null
+++ b/common/totp/totp_test.go
@@ -0,0 +1,49 @@
+package totp
+
+import (
+ "fmt"
+ "testing"
+)
+
+
+// 开启二次认证
+func _initAuth(user string) (secret, code string) {
+
+ ng := NewGoogleAuth()
+ // 秘钥
+ secret = ng.GetSecret()
+ fmt.Println("Secret:", secret)
+
+ // 动态码(每隔30s会动态生成一个6位数的数字)
+ code, err := ng.GetCode(secret)
+ fmt.Println("Code:", code, err)
+
+ // 用户名
+ qrCode := ng.GetQrcode(user, code, "ToughDemo")
+ fmt.Println("Qrcode", qrCode)
+
+ // 打印二维码地址
+ qrCodeUrl := ng.GetQrcodeUrl(user, secret, "ToughDemo")
+ fmt.Println("QrcodeUrl", qrCodeUrl)
+
+ return
+}
+
+
+func TestOTP(t *testing.T) {
+ // fmt.Println("-----------------开启二次认证----------------------")
+ user := "testxxx@qq.com"
+ secret, code := _initAuth(user)
+ fmt.Println(secret, code)
+
+ fmt.Println("-----------------信息校验----------------------")
+
+ // secret最好持久化保存在
+ // 验证,动态码(从谷歌验证器获取或者freeotp获取)
+ bool, err := NewGoogleAuth().VerifyCode(secret, code)
+ if bool {
+ fmt.Println("√")
+ } else {
+ fmt.Println("X", err)
+ }
+}
\ No newline at end of file
diff --git a/toughradius/radius_auth.go b/toughradius/radius_auth.go
index 0902dd21..750eb719 100644
--- a/toughradius/radius_auth.go
+++ b/toughradius/radius_auth.go
@@ -83,23 +83,64 @@ func (s *AuthService) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
s.CheckRadAuthError(username, ip, s.CheckAuthRateLimit(username))
}
- if isEap && eapmsg.Code == EAPCodeResponse && eapmsg.Type == EAPTypeIdentity {
- switch eapMethod {
- case EapMd5Method:
+ processEapType := func(eaptype byte) error {
+ switch eaptype {
+ case EAPTypeMD5Challenge:
// 发送EAP-Request/MD5-Challenge消息
err = s.sendEapMD5ChallengeRequest(w, r, vpe.Secret)
if err != nil {
- s.CheckRadAuthError(username, ip, fmt.Errorf("eap: send eap request error: %s", err))
+ return fmt.Errorf("eap: sendEapMD5ChallengeRequest error: %s", err)
}
- return
- case EapMschapv2Method:
+ case EAPTypeMSCHAPv2:
// 发送EAP-Request/MSCHAPv2-Challenge消息
err = s.sendEapMsChapV2Request(w, r, vpe.Secret)
if err != nil {
- s.CheckRadAuthError(username, ip, fmt.Errorf("eap: send eap request error: %s", err))
+ return fmt.Errorf("eap: sendEapMsChapV2Request error: %s", err)
}
+ case EAPTypeOTP:
+ // 发送 EAP-Request/OTP-Challenge
+ err = s.sendEapOTPChallengeRequest(w, r, vpe.Secret)
+ if err != nil {
+ return fmt.Errorf("eap: sendEapOTPChallengeRequest error: %s", err)
+ }
+ default:
+ return fmt.Errorf("eap: unsupported eap type: %d", eaptype)
+ }
+ return nil
+ }
+
+ // process EAP-Response/Identity
+ if isEap && eapmsg.Code == EAPCodeResponse && eapmsg.Type == EAPTypeIdentity {
+ eaptype := byte(0x00)
+ switch eapMethod {
+ case EapMd5Method:
+ eaptype = EAPTypeMD5Challenge
+ case EapMschapv2Method:
+ eaptype = EAPTypeMSCHAPv2
+ case EapOTPMethod:
+ eaptype = EAPTypeOTP
+ }
+ err := processEapType(eaptype)
+ if err != nil {
+ s.CheckRadAuthError(username, ip, fmt.Errorf("eap: processEapType error: %s", err))
+ } else {
return
+ }
+ }
+ // Process EAPTypeNak
+ if isEap && eapmsg.Type == EAPTypeNak {
+ if len(eapmsg.Data) == 0 {
+ fmt.Println("No alternative EAP methods suggested.")
+ return
+ }
+ for _, eapMethod := range eapmsg.Data {
+ err := processEapType(eapMethod)
+ if err != nil {
+ s.CheckRadAuthError(username, ip, fmt.Errorf("eap: processEapType error: %s", err))
+ } else {
+ return
+ }
}
}
@@ -167,6 +208,22 @@ func (s *AuthService) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
eapState.Success = true
sendAccept()
+ case EAPTypeOTP:
+ stateid := rfc2865.State_GetString(r.Packet)
+ eapState, err := s.GetEapState(stateid)
+ if err != nil {
+ s.SendEapFailureReject(w, r, vpe.Secret, fmt.Errorf("eap: get eap state error"))
+ return
+ }
+
+ otpPassword := "123456"
+ if string(eapmsg.Data) != otpPassword {
+ s.SendEapFailureReject(w, r, vpe.Secret, fmt.Errorf("eap: verify otp response error"))
+ return
+ }
+ eapState.Success = true
+ sendAccept()
+
case EAPTypeMSCHAPv2:
opcode, err := parseEAPMSCHAPv2OpCode(r.Packet)
if err != nil {
diff --git a/toughradius/radius_eap.go b/toughradius/radius_eap.go
index a13c07d7..e93373d3 100644
--- a/toughradius/radius_eap.go
+++ b/toughradius/radius_eap.go
@@ -33,6 +33,7 @@ const (
EapSakeMethod = "eap-sake"
EapIkev2Method = "eap-ikev2"
EapTncMethod = "eap-tnc"
+ EapOTPMethod = "eap-otp"
)
const (
@@ -207,7 +208,7 @@ func (s *AuthService) sendEapMD5ChallengeRequest(w radius.ResponseWriter, r *rad
rfc2865.State_SetString(resp, state)
// 创建EAP-Request/MD5-Challenge消息
- eapMessage := []byte{0x01, r.Identifier, 0x00, 0x16, 0x04, 0x10}
+ eapMessage := []byte{0x01, r.Identifier, 0x00, 0x16, EAPTypeMD5Challenge, 0x10}
eapMessage = append(eapMessage, eapChallenge...)
// 设置EAP-Message属性
@@ -235,3 +236,42 @@ func (s *AuthService) verifyEapMD5Response(eapid uint8, password string, challen
expectedResponse := hash.Sum(nil)
return bytes.Equal(expectedResponse, response[1:])
}
+
+
+// sendEapOTPChallengeRequest
+func (s *AuthService) sendEapOTPChallengeRequest(w radius.ResponseWriter, r *radius.Request, secret string) error {
+ // 创建一个新的RADIUS响应
+ var resp = r.Response(radius.CodeAccessChallenge)
+
+ eapChallenge := []byte("Please enter a one-time password")
+
+ state := common.UUID()
+ s.AddEapState(state, rfc2865.UserName_GetString(r.Packet), eapChallenge, EapOTPMethod)
+
+ rfc2865.State_SetString(resp, state)
+
+ // 创建EAP-Request/OTP-Challenge消息
+ eapMessage := []byte{0x01, r.Identifier}
+ eapMessage = append(eapMessage, []byte{0x00, 0x00}...) // Length, will be set later
+ eapMessage = append(eapMessage, EAPTypeOTP) // Type for EAP-OTP
+ eapMessage = append(eapMessage, eapChallenge...)
+
+ // Set the length
+ binary.BigEndian.PutUint16(eapMessage[2:4], uint16(len(eapMessage)))
+
+ // 设置EAP-Message属性
+ rfc2869.EAPMessage_Set(resp, eapMessage)
+ rfc2869.MessageAuthenticator_Set(resp, make([]byte, 16))
+
+ authenticator := generateMessageAuthenticator(resp, secret)
+ // 设置Message-Authenticator属性
+ rfc2869.MessageAuthenticator_Set(resp, authenticator)
+
+ // debug message
+ if app.GConfig().Radiusd.Debug {
+ log.Info(FmtResponse(resp, r.RemoteAddr))
+ }
+
+ // 发送RADIUS响应
+ return w.Write(resp)
+}
\ No newline at end of file
diff --git a/toughradius/radius_eap_otp_test.go b/toughradius/radius_eap_otp_test.go
new file mode 100644
index 00000000..aa9f69cb
--- /dev/null
+++ b/toughradius/radius_eap_otp_test.go
@@ -0,0 +1,124 @@
+package toughradius
+
+import (
+ "context"
+ "encoding/binary"
+ "fmt"
+ "net"
+ "testing"
+ "time"
+
+ "layeh.com/radius"
+ "layeh.com/radius/rfc2865"
+ "layeh.com/radius/rfc2869"
+)
+
+func TestEAPOTP(t *testing.T) {
+ // RADIUS 服务器配置
+ address := "127.0.0.1:1812" // 替换为 RADIUS 服务器地址
+ secret := []byte("secret") // 替换为 RADIUS 共享密钥
+
+ // 创建 RADIUS 客户端
+ client := radius.Client{}
+
+ // 创建 RADIUS 认证请求
+ request := radius.New(radius.CodeAccessRequest, secret)
+ rfc2865.CallingStationID_SetString(request, "10.10.10.10")
+ rfc2865.NASIdentifier_Set(request, []byte("tradtest"))
+ rfc2865.NASIPAddress_Set(request, net.ParseIP("127.0.0.1"))
+ rfc2865.NASPort_Set(request, 0)
+ rfc2865.NASPortType_Set(request, 0)
+ rfc2869.NASPortID_Set(request, []byte("slot=2;subslot=2;port=22;vlanid=100;"))
+ rfc2865.CalledStationID_SetString(request, "11:11:11:11:11:11")
+ rfc2865.CallingStationID_SetString(request, "11:11:11:11:11:11")
+
+ // 设置用户名
+ username := "test01"
+ rfc2865.UserName_SetString(request, username)
+
+ // 构建 EAP-Response/Identity 消息
+ eapIdentity := []byte{2, 1} // 2-Response, 1-Id
+ eapIdentity = append(eapIdentity, []byte{0x00, 0x00}...) // Length, will be set later
+ eapIdentity = append(eapIdentity, 1) // EAP Type = Identity
+ eapIdentity = append(eapIdentity, []byte(username)...)
+
+ // Set the length
+ binary.BigEndian.PutUint16(eapIdentity[2:4], uint16(len(eapIdentity)))
+
+ rfc2869.EAPMessage_Set(request, eapIdentity)
+
+ // 发送 RADIUS 请求,并接收响应
+ fmt.Println(FmtPacket(request))
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ response, err := client.Exchange(ctx, request, address)
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+ fmt.Println(FmtPacket(response))
+
+ // 检查响应类型
+ if response.Code != radius.CodeAccessChallenge {
+ fmt.Println("Unexpected response code:", response.Code)
+ return
+ }
+
+ stateId := rfc2865.State_GetString(response)
+
+ // 处理 EAP-Request/OTP 挑战
+ eapChallenge := response.Attributes.Get(rfc2869.EAPMessage_Type)
+ if eapChallenge == nil {
+ fmt.Println("EAP-Request/OTP challenge not received")
+ return
+ }
+
+ // 提取 EAP-Request/OTP 挑战中的标识符
+ eapID := eapChallenge[1]
+
+ // 构建 EAP-Response/OTP 消息
+ otp := "123456" // 模拟的 OTP 值
+ eapResponse := []byte{2, eapID} // 2-Response, EapID
+ eapResponse = append(eapResponse, []byte{0x00, 0x00}...) // Length, will be set later
+ eapResponse = append(eapResponse, 5) // EAP Type = OTP
+ eapResponse = append(eapResponse, []byte(otp)...)
+
+ // Set the length
+ binary.BigEndian.PutUint16(eapResponse[2:4], uint16(len(eapResponse)))
+
+ // 创建新的 RADIUS 请求以响应 OTP 挑战
+ request = radius.New(radius.CodeAccessRequest, secret)
+ rfc2865.CallingStationID_SetString(request, "10.10.10.10")
+ rfc2865.NASIdentifier_Set(request, []byte("tradtest"))
+ rfc2865.NASIPAddress_Set(request, net.ParseIP("127.0.0.1"))
+ rfc2865.NASPort_Set(request, 0)
+ rfc2865.NASPortType_Set(request, 0)
+ rfc2865.State_SetString(request, stateId)
+ rfc2869.NASPortID_Set(request, []byte("slot=2;subslot=2;port=22;vlanid=100;"))
+ rfc2865.CalledStationID_SetString(request, "11:11:11:11:11:11")
+ rfc2865.CallingStationID_SetString(request, "11:11:11:11:11:11")
+
+ rfc2865.UserName_SetString(request, username)
+ rfc2869.EAPMessage_Set(request, eapResponse)
+
+ // 再次发送 RADIUS 请求,并接收响应
+ fmt.Println(FmtPacket(request))
+
+ ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ response, err = client.Exchange(ctx, request, address)
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+ fmt.Println(FmtPacket(response))
+
+ // 检查最终的 RADIUS 响应
+ if response.Code == radius.CodeAccessAccept {
+ fmt.Println("Authentication successful")
+ } else if response.Code == radius.CodeAccessReject {
+ fmt.Println("Authentication failed")
+ } else {
+ fmt.Println("Unexpected response code:", response.Code)
+ }
+}