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) + } +}