diff --git a/client/auth/sig.go b/client/auth/sig.go index 6f3a3414..d9b7af90 100644 --- a/client/auth/sig.go +++ b/client/auth/sig.go @@ -14,13 +14,15 @@ var ( ) type SigInfo struct { - Uin uint32 - Sequence uint32 - UID string - Tgtgt []byte - Tgt []byte - D2 []byte - D2Key []byte + Uin uint32 + Sequence uint32 + UID string + + Tgtgt []byte + Tgt []byte + D2 []byte + D2Key []byte + Qrsig []byte ExchangeKey []byte KeySig []byte @@ -29,6 +31,9 @@ type SigInfo struct { TempPwd []byte CaptchaInfo [3]string + CaptchaURL string + NewDeviceVerifyURL string + Nickname string Age uint8 Gender uint8 diff --git a/client/base.go b/client/base.go index 11e11ff9..002f3669 100644 --- a/client/base.go +++ b/client/base.go @@ -3,6 +3,7 @@ package client // 部分借鉴 https://github.com/Mrs4s/MiraiGo/blob/master/client/client.go import ( + "crypto/md5" "errors" "net/http" "net/http/cookiejar" @@ -30,11 +31,16 @@ import ( ) // NewClient 创建一个新的 QQ Client -func NewClient(uin uint32, appInfo *auth.AppInfo, signURL ...string) *QQClient { +func NewClient(uin uint32, password string, appInfo *auth.AppInfo, signURL ...string) *QQClient { + return NewClientMD5(uin, md5.Sum([]byte(password)), appInfo, signURL...) +} + +func NewClientMD5(uin uint32, passwordMD5 [16]byte, appInfo *auth.AppInfo, signURL ...string) *QQClient { cookieContainer, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) client := &QQClient{ - Uin: uin, - oicq: oicq.NewCodec(int64(uin)), + Uin: uin, + oicq: oicq.NewCodec(int64(uin)), + passwordMD5: passwordMD5, highwaySession: highway.Session{ AppID: uint32(appInfo.AppID), SubAppID: uint32(appInfo.SubAppID), @@ -65,8 +71,9 @@ type QQClient struct { Online atomic.Bool - t106 []byte - t16a []byte + t106 []byte + t16a []byte + passwordMD5 [16]byte UA string diff --git a/client/client.go b/client/client.go index 6e586e02..7dba4777 100644 --- a/client/client.go +++ b/client/client.go @@ -1,111 +1,29 @@ package client import ( + "bytes" + "encoding/base64" + "encoding/json" "errors" "fmt" - "os" + "io" + "net/http" + "net/url" + "strings" "time" - "github.com/LagrangeDev/LagrangeGo/client/auth" - "github.com/LagrangeDev/LagrangeGo/client/packets/tlv" "github.com/LagrangeDev/LagrangeGo/client/packets/wtlogin" "github.com/LagrangeDev/LagrangeGo/client/packets/wtlogin/loginstate" "github.com/LagrangeDev/LagrangeGo/client/packets/wtlogin/qrcodestate" "github.com/LagrangeDev/LagrangeGo/utils" "github.com/LagrangeDev/LagrangeGo/utils/binary" - "github.com/LagrangeDev/LagrangeGo/utils/crypto" ) -func (c *QQClient) Login(password, qrcodePath string) error { - // prefer session login - if len(c.transport.Sig.D2) != 0 && c.transport.Sig.Uin != 0 { - c.infoln("Session found, try to login with session") - c.Uin = c.transport.Sig.Uin - if c.Online.Load() { - return ErrAlreadyOnline - } - err := c.connect() - if err != nil { - return err - } - err = c.Register() - if err != nil { - err = fmt.Errorf("failed to register session: %v", err) - c.errorln(err) - return err - } - return nil - } - - if len(c.transport.Sig.TempPwd) != 0 { - err := c.keyExchange() - if err != nil { - return err - } - - ret, err := c.TokenLogin() - if err != nil { - return fmt.Errorf("EasyLogin fail: %s", err) - } - - if ret.Successful() { - return c.Register() - } - } - - if password != "" { - c.infoln("login with password") - err := c.keyExchange() - if err != nil { - return err - } - - for { - ret, err := c.PasswordLogin(password) - if err != nil { - return err - } - switch { - case ret.Successful(): - return c.Register() - case ret == loginstate.CaptchaVerify: - c.warningln("captcha verification required") - c.infoln("ticket?->") - c.transport.Sig.CaptchaInfo[0] = utils.ReadLine() - c.infoln("rand_str?->") - c.transport.Sig.CaptchaInfo[1] = utils.ReadLine() - default: - c.error("Unhandled exception raised: %s", ret.Name()) - } - } - // panic("unreachable") - } - c.infoln("login with qrcode") - png, _, err := c.FetchQRCodeDefault() - if err != nil { - return err - } - err = os.WriteFile(qrcodePath, png, 0666) - if err != nil { - return err - } - c.info("qrcode saved to %s", qrcodePath) - err = c.QRCodeLogin(3) - if err != nil { - return err - } - return c.Register() -} - func (c *QQClient) TokenLogin() (loginstate.State, error) { if c.Online.Load() { return -996, ErrAlreadyOnline } - err := c.connect() - if err != nil { - return -997, err - } data, err := buildNtloginRequest(c.Uin, c.version(), c.Device(), &c.transport.Sig, c.transport.Sig.TempPwd) if err != nil { return -998, err @@ -224,36 +142,27 @@ func (c *QQClient) keyExchange() error { data, ) if err != nil { - c.errorln(err) return err } - c.debug("keyexchange proto data: %x", packet) - c.transport.Sig.ExchangeKey, c.transport.Sig.KeySig, err = wtlogin.ParseKeyExchangeResponse(packet) - return err + return wtlogin.ParseKeyExchangeResponse(packet, c.Sig()) } -func (c *QQClient) PasswordLogin(password string) (loginstate.State, error) { +func (c *QQClient) PasswordLogin() (loginstate.State, error) { if c.Online.Load() { - return -996, ErrAlreadyOnline + return -994, ErrAlreadyOnline } + err := c.connect() if err != nil { - return -997, err + return -995, err } - md5Password := crypto.MD5Digest(utils.S2B(password)) - - cr := tlv.T106( - c.version().AppID, - c.version().AppClientVersion, - int(c.Uin), - c.Device().GUID, - md5Password, - c.transport.Sig.Tgtgt, - make([]byte, 4), - true)[4:] + err = c.keyExchange() + if err != nil { + return -996, err + } - data, err := buildNtloginRequest(c.Uin, c.version(), c.Device(), &c.transport.Sig, cr) + data, err := buildPasswordLoginRequest(c.Uin, c.version(), c.Device(), &c.transport.Sig, c.passwordMD5) if err != nil { return -998, err } @@ -267,27 +176,141 @@ func (c *QQClient) PasswordLogin(password string) (loginstate.State, error) { return parseNtloginResponse(packet, &c.transport.Sig) } -func (c *QQClient) QRCodeLogin(refreshInterval int) error { - if c.transport.Sig.Qrsig == nil { - return errors.New("no QrSig found, fetch qrcode first") +func (c *QQClient) CommitCaptcha(ticket, randStr, aid string) (loginstate.State, error) { + c.Sig().CaptchaInfo = [3]string{ticket, randStr, aid} + data, err := buildPasswordLoginRequest(c.Uin, c.version(), c.Device(), &c.transport.Sig, c.passwordMD5) + if err != nil { + return -998, err + } + packet, err := c.sendUniPacketAndWait( + "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLogin", + data, + ) + if err != nil { + return -999, err + } + ret, err := parseNtloginResponse(packet, &c.transport.Sig) + if err != nil { + return -999, err + } + if ret.Successful() { + return ret, c.init() + } + return ret, nil +} + +func (c *QQClient) GetNewDeviceVerifyURL() (string, error) { + if c.Sig().NewDeviceVerifyURL == "" { + return "", errors.New("no verify_url found") } - for { - retCode, err := c.GetQRCodeResult() + queryParams := func() url.Values { + parsedURL, err := url.Parse(c.Sig().NewDeviceVerifyURL) + if err != nil { + return url.Values{} + } + return parsedURL.Query() + }() + + request, _ := json.Marshal(&NTNewDeviceQrCodeRequest{ + StrDevAuthToken: queryParams.Get("sig"), + Uint32Flag: 1, + Uint32UrlType: 0, + StrUinToken: queryParams.Get("uin-token"), + StrDevType: c.version().OS, + StrDevName: c.Device().DeviceName, + }) + + resp, err := func() (*NTNewDeviceQrCodeResponse, error) { + resp, err := http.Post(fmt.Sprintf("https://oidb.tim.qq.com/v3/oidbinterface/oidb_0xc9e_8?uid=%d&getqrcode=1&sdkappid=39998&actype=2", c.Uin), + "application/json", bytes.NewReader(request)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + var response NTNewDeviceQrCodeResponse + err = json.Unmarshal(data, &response) if err != nil { - c.errorln(err) - return err + return nil, err } - if retCode.Waitable() { - time.Sleep(time.Duration(refreshInterval) * time.Second) + return &response, nil + }() + if err != nil { + return "", err + } + return resp.StrURL, nil +} + +func (c *QQClient) NewDeviceVerify(verifyURL string) error { + original := func() string { + params := strings.FieldsFunc(strings.Split(verifyURL, "?")[1], func(r rune) bool { + return r == '&' + }) + for _, param := range params { + if strings.HasPrefix(param, "str_url=") { + return strings.TrimPrefix(param, "str_url=") + } + } + return "" + }() + + query := func() []byte { + data, _ := json.Marshal(&NTNewDeviceQrCodeQuery{ + Uint32Flag: 0, + Token: base64.StdEncoding.EncodeToString(utils.S2B(original)), + }) + return data + }() + for errCount, timeSec := 0, 0; timeSec < 120 || errCount < 3; timeSec++ { + resp, err := func() (*NTNewDeviceQrCodeResponse, error) { + resp, err := http.Post(fmt.Sprintf("https://oidb.tim.qq.com/v3/oidbinterface/oidb_0xc9e_8?uid=%d&getqrcode=1&sdkappid=39998&actype=2", c.Uin), + "application/json", bytes.NewReader(query)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + var response NTNewDeviceQrCodeResponse + err = json.Unmarshal(data, &response) + if err != nil { + return nil, err + } + return &response, nil + }() + if err != nil { + errCount++ continue } - if !retCode.Success() { - return errors.New(retCode.Name()) + if resp.StrNtSuccToken != "" { + c.transport.Sig.TempPwd = utils.S2B(resp.StrNtSuccToken) + data, err := buildNtloginRequest(c.Uin, c.version(), c.Device(), &c.transport.Sig, c.transport.Sig.TempPwd) + if err != nil { + return err + } + + packet, err := c.sendUniPacketAndWait( + "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginNewDevice", + data, + ) + if err != nil { + return err + } + ret, err := parseNewDeviceLoginResponse(packet, &c.transport.Sig) + if err != nil { + return err + } + if ret.Successful() { + return c.init() + } + return fmt.Errorf("verify error: %s", ret.Name()) } - break + time.Sleep(1 * time.Second) } + return fmt.Errorf("verify timeout error") +} +func (c *QQClient) QRCodeLogin() error { app := c.version() device := c.Device() response, err := c.sendUniPacketAndWait( @@ -313,16 +336,19 @@ func (c *QQClient) QRCodeLogin(refreshInterval int) error { ).ToBytes())) if err != nil { - c.errorln(err) return err } - return c.decodeLoginResponse(response, &c.transport.Sig) + err = c.decodeLoginResponse(response, &c.transport.Sig) + if err != nil { + return err + } + return c.init() } -func (c *QQClient) FastLogin(sig *auth.SigInfo) error { - if sig != nil { - c.UseSig(*sig) +func (c *QQClient) FastLogin() error { + if c.transport.Sig.Uin == 0 || len(c.transport.Sig.D2) == 0 { + return errors.New("no login cache") } c.Uin = c.transport.Sig.Uin if c.Online.Load() { @@ -332,16 +358,19 @@ func (c *QQClient) FastLogin(sig *auth.SigInfo) error { if err != nil { return err } - err = c.Register() + err = c.register() if err != nil { - err = fmt.Errorf("failed to register session: %v", err) - c.errorln(err) - return err + return fmt.Errorf("failed to register session: %v", err) + } return nil } -func (c *QQClient) Register() error { +func (c *QQClient) init() error { + return c.register() +} + +func (c *QQClient) register() error { response, err := c.sendUniPacketAndWait( "trpc.qq_new_tech.status_svc.StatusService.Register", wtlogin.BuildRegisterRequest(c.version(), c.Device())) @@ -359,6 +388,30 @@ func (c *QQClient) Register() error { c.transport.Sig.Uin = c.Uin c.setOnline() go c.doHeartbeat() - c.infoln("register succeeded") return nil } + +type ( + NTNewDeviceQrCodeRequest struct { + StrDevAuthToken string `json:"str_dev_auth_token"` + Uint32Flag int `json:"uint32_flag"` + Uint32UrlType int `json:"uint32_url_type"` + StrUinToken string `json:"str_uin_token"` + StrDevType string `json:"str_dev_type"` + StrDevName string `json:"str_dev_name"` + } + + NTNewDeviceQrCodeResponse struct { + Uint32GuaranteeStatus int `json:"uint32_guarantee_status"` + StrURL string `json:"str_url"` + ActionStatus string `json:"ActionStatus"` + StrNtSuccToken string `json:"str_nt_succ_token"` + ErrorCode int `json:"ErrorCode"` + ErrorInfo string `json:"ErrorInfo"` + } + + NTNewDeviceQrCodeQuery struct { + Uint32Flag uint64 `json:"uint32_flag"` + Token string `json:"bytes_token"` + } +) diff --git a/client/internal/network/response.go b/client/internal/network/response.go index 90761813..5f9116f8 100644 --- a/client/internal/network/response.go +++ b/client/internal/network/response.go @@ -26,9 +26,10 @@ type Response struct { } var ( - ErrSessionExpired = errors.New("session expired") - ErrPacketDropped = errors.New("packet dropped") - ErrInvalidPacketType = errors.New("invalid packet type") + ErrSessionExpired = errors.New("session expired") + ErrAuthenticationFailed = errors.New("authentication failed") + ErrPacketDropped = errors.New("packet dropped") + ErrInvalidPacketType = errors.New("invalid packet type") ) func (t *Transport) ReadResponse(head []byte) (*Response, error) { @@ -65,15 +66,22 @@ func (t *Transport) readSSOFrame(resp *Response, payload []byte) error { head := binary.NewReader(reader.ReadBytes(int(headLen) - 4)) resp.SequenceID = head.ReadI32() - switch retCode := head.ReadI32(); retCode { + retCode := head.ReadI32() + resp.Message = head.ReadStringWithLength("u32", true) + var err error + switch retCode { case 0: // ok - case -10001, -10003, -10008: // -10001正常缓存过期,-10003登录失效? - return errors.WithStack(ErrSessionExpired) + case -10001, -10008: // -10001正常缓存过期,-10003登录失效? + err = ErrSessionExpired + case -10003: + err = ErrAuthenticationFailed default: - return errors.Errorf("return code unsuccessful: %d", retCode) + err = errors.Errorf("return code unsuccessful: %d", retCode) + } + if err != nil { + return errors.Errorf("%s %s", err.Error(), resp.Message) } - resp.Message = head.ReadStringWithLength("u32", true) resp.CommandName = head.ReadStringWithLength("u32", true) if resp.CommandName == "Heartbeat.Alive" { return nil diff --git a/client/log.go b/client/log.go index 9fc79a1c..3313fbde 100644 --- a/client/log.go +++ b/client/log.go @@ -1,3 +1,4 @@ +//nolint:unused package client // from https://github.com/Mrs4s/MiraiGo/blob/master/client/log.go diff --git a/client/network.go b/client/network.go index 86a12568..f93f8c28 100644 --- a/client/network.go +++ b/client/network.go @@ -5,6 +5,7 @@ import ( "net/netip" "runtime/debug" "sort" + "strings" "sync" "time" @@ -125,7 +126,7 @@ func (c *QQClient) quickReconnect() { c.DisconnectedEvent.dispatch(c, &DisconnectedEvent{Message: "quick reconnect failed"}) return } - if err := c.Register(); err != nil { + if err := c.register(); err != nil { c.error("register client failed: %v", err) c.Disconnect() c.DisconnectedEvent.dispatch(c, &DisconnectedEvent{Message: "register error"}) @@ -272,7 +273,7 @@ func (c *QQClient) unexpectedDisconnect(_ *network.TCPClient, e error) { c.DisconnectedEvent.dispatch(c, &DisconnectedEvent{Message: "connection dropped by server."}) return } - if err := c.Register(); err != nil { + if err := c.register(); err != nil { c.error("register client failed: %v", err) c.Disconnect() c.DisconnectedEvent.dispatch(c, &DisconnectedEvent{Message: "register error"}) @@ -304,7 +305,7 @@ func (c *QQClient) netLoop() { c.error("parse incoming packet error: %v", err) if errors.Is(err, network.ErrSessionExpired) || errors.Is(err, network.ErrPacketDropped) { c.Disconnect() - go c.DisconnectedEvent.dispatch(c, &DisconnectedEvent{Message: "session expired"}) + go c.DisconnectedEvent.dispatch(c, &DisconnectedEvent{Message: err.Error()}) continue } errCount++ @@ -313,7 +314,7 @@ func (c *QQClient) netLoop() { } continue } - if resp.EncryptType == network.EncryptTypeEmptyKey { + if resp.EncryptType == network.EncryptTypeEmptyKey && strings.HasPrefix(resp.CommandName, "wtlogin") { m, err := c.oicq.Unmarshal(resp.Body) if err != nil { c.error("decrypt payload error: %v", err) diff --git a/client/ntlogin.go b/client/ntlogin.go index a7980b5d..f817a882 100644 --- a/client/ntlogin.go +++ b/client/ntlogin.go @@ -1,10 +1,13 @@ package client import ( + "errors" "fmt" - "net/url" "strconv" + "github.com/LagrangeDev/LagrangeGo/utils/binary" + ftea "github.com/fumiama/gofastTEA" + "github.com/LagrangeDev/LagrangeGo/client/auth" "github.com/LagrangeDev/LagrangeGo/client/packets/pb/login" "github.com/LagrangeDev/LagrangeGo/client/packets/wtlogin/loginstate" @@ -13,54 +16,91 @@ import ( "github.com/LagrangeDev/LagrangeGo/utils/crypto" ) -func buildNtloginCaptchaSubmit(ticket, randStr, aid string) proto.DynamicMessage { - return proto.DynamicMessage{ - 1: ticket, - 2: randStr, - 3: aid, - } +func buildPasswordLoginRequest(uin uint32, app *auth.AppInfo, device *auth.DeviceInfo, sig *auth.SigInfo, passwordMD5 [16]byte) ([]byte, error) { + key := crypto.MD5Digest(binary.NewBuilder(nil). + WriteBytes(passwordMD5[:]). + WriteU32(0). + WriteU32(uin). + ToBytes(), + ) + + plainBytes := binary.NewBuilder(nil). + WriteU16(4). + WriteU32(crypto.RandU32()). + WriteU32(0). + WriteU32(uint32(app.AppID)). + WriteU32(8001). + WriteU32(0). + WriteU32(uin). + WriteU32(uint32(utils.TimeStamp())). + WriteU32(0). + WriteU8(1). + WriteBytes(passwordMD5[:]). + WriteBytes(crypto.RandomBytes(16)). + WriteU32(0). + WriteU8(1). + WriteBytes(utils.MustParseHexStr(device.GUID)). + WriteU32(1). + WriteU32(1). + WritePacketString(strconv.Itoa(int(uin)), "u16", false). + ToBytes() + + encryptedPlain := ftea.NewTeaCipher(key).Encrypt(plainBytes) + return buildNtloginRequest(uin, app, device, sig, encryptedPlain) } func buildNtloginRequest(uin uint32, app *auth.AppInfo, device *auth.DeviceInfo, sig *auth.SigInfo, credential []byte) ([]byte, error) { - body := proto.DynamicMessage{ - 1: proto.DynamicMessage{ - 1: proto.DynamicMessage{ - 1: strconv.Itoa(int(uin)), - }, - 2: proto.DynamicMessage{ - 1: app.OS, - 2: device.DeviceName, - 3: app.NTLoginType, - 4: utils.MustParseHexStr(device.GUID), + if sig.ExchangeKey == nil || sig.KeySig == nil { + return nil, errors.New("empty key") + } + + packet := login.SsoNTLoginBase{ + Header: &login.SsoNTLoginHeader{ + Uin: &login.SsoNTLoginUin{Uin: proto.Some(strconv.Itoa(int(uin)))}, + System: &login.SsoNTLoginSystem{ + OS: proto.Some(app.OS), + DeviceName: proto.Some(device.DeviceName), + Type: int32(app.NTLoginType), + Guid: utils.MustParseHexStr(device.GUID), }, - 3: proto.DynamicMessage{ - 1: device.KernelVersion, - 2: app.AppID, - 3: app.PackageName, + Version: &login.SsoNTLoginVersion{ + KernelVersion: proto.Some(device.KernelVersion), + AppId: int32(app.AppID), + PackageName: proto.Some(app.PackageName), }, + Cookie: &login.SsoNTLoginCookie{Cookie: proto.Some(sig.Cookies)}, }, - 2: proto.DynamicMessage{ - 1: credential, - }, + Body: func() []byte { + body := login.SsoNTLoginEasyLogin{ + TempPassword: credential, + } + if all(sig.CaptchaInfo[:3]) { + body.Captcha = &login.SsoNTLoginCaptchaSubmit{ + Ticket: sig.CaptchaInfo[0], + RandStr: sig.CaptchaInfo[1], + Aid: sig.CaptchaInfo[2], + } + } + b, _ := proto.Marshal(&body) + return b + }(), } - if sig.Cookies != "" { - body[1].(proto.DynamicMessage)[5] = proto.DynamicMessage{1: sig.Cookies} - } - if all(sig.CaptchaInfo[:3]) { - body[2].(proto.DynamicMessage)[2] = buildNtloginCaptchaSubmit(sig.CaptchaInfo[0], sig.CaptchaInfo[1], sig.CaptchaInfo[2]) + pkt, err := proto.Marshal(&packet) + if err != nil { + return nil, err } - data, err := crypto.AESGCMEncrypt(body.Encode(), sig.ExchangeKey) + data, err := crypto.AESGCMEncrypt(pkt, sig.ExchangeKey) if err != nil { return nil, err } - return proto.DynamicMessage{ - 1: sig.KeySig, - 3: data, - 4: 1, - }.Encode(), nil + return proto.Marshal(&login.SsoNTLoginEncryptedData{ + Sign: sig.KeySig, + GcmCalc: data, + Type: 1, + }) } func parseNtloginResponse(response []byte, sig *auth.SigInfo) (loginstate.State, error) { @@ -93,12 +133,22 @@ func parseNtloginResponse(response []byte, sig *auth.SigInfo) (loginstate.State, return loginstate.Success, nil } ret := loginstate.State(base.Header.Error.ErrorCode) - if ret == loginstate.CaptchaVerify { + if base.Header.Error != nil && ret.NeedVerify() { + sig.UnusualSig = func() []byte { + if body.Unusual != nil { + return body.Unusual.Sig + } + return nil + }() sig.Cookies = base.Header.Cookie.Cookie.Unwrap() - verifyURL := body.Captcha.Url - aid := getAid(verifyURL) - sig.CaptchaInfo[2] = aid - return ret, fmt.Errorf("need captcha verify: %v", verifyURL) + sig.CaptchaURL = func() string { + if body.Captcha != nil { + return body.Captcha.Url + } + return "" + }() + sig.NewDeviceVerifyURL = base.Header.Error.NewDeviceVerifyUrl.Unwrap() + return ret, nil } if base.Header.Error.Tag != "" { stat := base.Header.Error @@ -109,10 +159,43 @@ func parseNtloginResponse(response []byte, sig *auth.SigInfo) (loginstate.State, return ret, fmt.Errorf("login fail: %s", ret.Name()) } -func getAid(verifyURL string) string { - u, _ := url.Parse(verifyURL) - q := u.Query() - return q["sid"][0] +func parseNewDeviceLoginResponse(response []byte, sig *auth.SigInfo) (loginstate.State, error) { + if len(sig.ExchangeKey) == 0 { + return -999, errors.New("empty exchange key") + } + + var encrypted login.SsoNTLoginEncryptedData + err := proto.Unmarshal(response, &encrypted) + if err != nil { + return -999, err + } + + if encrypted.GcmCalc != nil { + decrypted, err := crypto.AESGCMDecrypt(encrypted.GcmCalc, sig.ExchangeKey) + if err != nil { + return -999, err + } + var base login.SsoNTLoginBase + err = proto.Unmarshal(decrypted, &base) + if err != nil { + return -999, err + } + var body login.SsoNTLoginResponse + err = proto.Unmarshal(base.Body, &body) + if err != nil { + return -999, err + } + ret := loginstate.State(base.Header.Error.ErrorCode) + if body.Credentials == nil { + return ret, errors.New(ret.Name()) + } + sig.Tgt = body.Credentials.Tgt + sig.D2 = body.Credentials.D2 + sig.D2Key = body.Credentials.D2Key + sig.TempPwd = body.Credentials.TempPassword + return ret, nil + } + return -999, errors.New("empty data") } func all(b []string) bool { diff --git a/client/packets/pb/login/ecdh.pb.go b/client/packets/pb/login/ecdh.pb.go index ef875395..1f7a0b27 100644 --- a/client/packets/pb/login/ecdh.pb.go +++ b/client/packets/pb/login/ecdh.pb.go @@ -28,6 +28,6 @@ type SsoKeyExchangeDecrypted struct { } type SsoKeyExchangePlain struct { - Uin proto.Option[uint32] `protobuf:"varint,1,opt"` + Uin proto.Option[string] `protobuf:"bytes,1,opt"` Guid []byte `protobuf:"bytes,2,opt"` } diff --git a/client/packets/pb/login/ecdh.proto b/client/packets/pb/login/ecdh.proto index 9704a089..aae23fb5 100644 --- a/client/packets/pb/login/ecdh.proto +++ b/client/packets/pb/login/ecdh.proto @@ -23,6 +23,6 @@ message SsoKeyExchangeDecrypted { } message SsoKeyExchangePlain { - optional uint32 Uin = 1; + optional string Uin = 1; optional bytes Guid = 2; } diff --git a/client/packets/wtlogin/exchange.go b/client/packets/wtlogin/exchange.go index d21e9fe8..3f2a8321 100644 --- a/client/packets/wtlogin/exchange.go +++ b/client/packets/wtlogin/exchange.go @@ -2,6 +2,10 @@ package wtlogin import ( "encoding/hex" + "strconv" + "time" + + "github.com/LagrangeDev/LagrangeGo/client/auth" "github.com/LagrangeDev/LagrangeGo/client/packets/pb/login" "github.com/LagrangeDev/LagrangeGo/internal/proto" @@ -14,10 +18,15 @@ import ( var encKey, _ = hex.DecodeString("e2733bf403149913cbf80c7a95168bd4ca6935ee53cd39764beebe2e007e3aee") func BuildKexExchangeRequest(uin uint32, guid string) ([]byte, error) { - encl, err := crypto.AESGCMEncrypt(proto.DynamicMessage{ - 1: uin, - 2: guid, - }.Encode(), ecdh.P256().SharedKey()) + plain1, err := proto.Marshal(&login.SsoKeyExchangePlain{ + Uin: proto.Some(strconv.Itoa(int(uin))), + Guid: utils.MustParseHexStr(guid), + }) + if err != nil { + return nil, err + } + + encl, err := crypto.AESGCMEncrypt(plain1, ecdh.P256().SharedKey()) if err != nil { return nil, err } @@ -36,36 +45,38 @@ func BuildKexExchangeRequest(uin uint32, guid string) ([]byte, error) { return nil, err } - return proto.DynamicMessage{ - 1: ecdh.P256().PublicKey(), - 2: 1, - 3: encl, - 4: utils.TimeStamp(), - 5: encP2Hash, - }.Encode(), nil + return proto.Marshal(&login.SsoKeyExchange{ + PubKey: ecdh.P256().PublicKey(), + Type: 1, + GcmCalc1: encl, + Timestamp: uint32(time.Now().Unix()), + GcmCalc2: encP2Hash, + }) } -func ParseKeyExchangeResponse(response []byte) (key, sign []byte, err error) { +func ParseKeyExchangeResponse(response []byte, sig *auth.SigInfo) error { var p login.SsoKeyExchangeResponse - err = proto.Unmarshal(response, &p) + err := proto.Unmarshal(response, &p) if err != nil { - return + return err } shareKey, err := ecdh.P256().Exange(p.PublicKey) if err != nil { - return + return err } var decPb login.SsoKeyExchangeDecrypted data, err := crypto.AESGCMDecrypt(p.GcmEncrypted, shareKey) if err != nil { - return + return err } err = proto.Unmarshal(data, &decPb) if err != nil { - return + return err } + sig.ExchangeKey = decPb.GcmKey + sig.KeySig = decPb.Sign - return decPb.GcmKey, decPb.Sign, nil + return nil } diff --git a/client/packets/wtlogin/loginstate/enum.go b/client/packets/wtlogin/loginstate/enum.go index 8d5a13b1..249c1f4b 100644 --- a/client/packets/wtlogin/loginstate/enum.go +++ b/client/packets/wtlogin/loginstate/enum.go @@ -45,3 +45,7 @@ func (r State) Missing() bool { func (r State) Successful() bool { return r == Success } + +func (r State) NeedVerify() bool { + return r == NewDeviceVerify || r == CaptchaVerify || r == UnusualVerify +} diff --git a/client/packets/wtlogin/statusService.go b/client/packets/wtlogin/statusService.go index 40f664db..dfc90103 100644 --- a/client/packets/wtlogin/statusService.go +++ b/client/packets/wtlogin/statusService.go @@ -3,12 +3,10 @@ package wtlogin import ( "errors" "strings" - "unicode" "github.com/LagrangeDev/LagrangeGo/client/auth" "github.com/LagrangeDev/LagrangeGo/client/packets/pb/system" "github.com/LagrangeDev/LagrangeGo/internal/proto" - "github.com/LagrangeDev/LagrangeGo/utils" ) // BuildRegisterRequest trpc.qq_new_tech.status_svc.StatusService.Register @@ -21,7 +19,7 @@ func BuildRegisterRequest(app *auth.AppInfo, device *auth.DeviceInfo) []byte { 5: 2052, 6: proto.DynamicMessage{ 1: device.DeviceName, - 2: capitalize(app.VendorOS), + 2: app.Kernel, 3: device.SystemKernel, 4: "", 5: app.VendorOS, @@ -53,11 +51,3 @@ func ParseRegisterResponse(response []byte) error { } return errors.New(msg) } - -func capitalize(s string) string { - news := make([]byte, len(s)) - rs := []rune(s) - n := copy(news, string(unicode.ToUpper(rs[0]))) - copy(news[n:], strings.ToLower(s[n:])) - return utils.B2S(news) -} diff --git a/client/sign/http.go b/client/sign/http.go index 446844ca..ecbab360 100644 --- a/client/sign/http.go +++ b/client/sign/http.go @@ -25,22 +25,23 @@ func init() { "trpc.o3.ecdh_access.EcdhAccess.SsoSecureAccess", "trpc.o3.report.Report.SsoReport", "MessageSvc.PbSendMsg", - // "wtlogin.trans_emp", + //"wtlogin.trans_emp", "wtlogin.login", - // "trpc.login.ecdh.EcdhService.SsoKeyExchange", + //"trpc.login.ecdh.EcdhService.SsoKeyExchange", "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLogin", "trpc.login.ecdh.EcdhService.SsoNTLoginEasyLogin", "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginNewDevice", "trpc.login.ecdh.EcdhService.SsoNTLoginEasyLoginUnusualDevice", "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginUnusualDevice", "OidbSvcTrpcTcp.0x11ec_1", - "OidbSvcTrpcTcp.0x758_1", - "OidbSvcTrpcTcp.0x7c2_5", + "OidbSvcTrpcTcp.0x758_1", // create group + "OidbSvcTrpcTcp.0x7c1_1", + "OidbSvcTrpcTcp.0x7c2_5", // request friend "OidbSvcTrpcTcp.0x10db_1", - "OidbSvcTrpcTcp.0x8a1_7", + "OidbSvcTrpcTcp.0x8a1_7", // request group "OidbSvcTrpcTcp.0x89a_0", "OidbSvcTrpcTcp.0x89a_15", - "OidbSvcTrpcTcp.0x88d_0", + "OidbSvcTrpcTcp.0x88d_0", // fetch group detail "OidbSvcTrpcTcp.0x88d_14", "OidbSvcTrpcTcp.0x112a_1", "OidbSvcTrpcTcp.0x587_74", diff --git a/main.go b/main.go index 018f9b82..06050354 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( + "errors" "fmt" "os" "os/signal" @@ -10,6 +11,8 @@ import ( "syscall" "time" + "github.com/LagrangeDev/LagrangeGo/client/packets/wtlogin/loginstate" + "github.com/LagrangeDev/LagrangeGo/client" "github.com/LagrangeDev/LagrangeGo/client/auth" "github.com/LagrangeDev/LagrangeGo/message" @@ -31,7 +34,7 @@ func main() { KernelVersion: "10.0.22631", } - qqclient := client.NewClient(0, appInfo, "https://sign.lagrangecore.org/api/sign") + qqclient := client.NewClient(0, "", appInfo, "https://sign.lagrangecore.org/api/sign/25765") qqclient.SetLogger(protocolLogger{}) qqclient.UseDevice(deviceInfo) data, err := os.ReadFile("sig.bin") @@ -64,26 +67,98 @@ func main() { } }) - err = qqclient.Login("", "qrcode.png") + err = func(c *client.QQClient) error { + err := c.FastLogin() + if err == nil { + return nil + } + + ret, err := c.PasswordLogin() + if err == nil { + return nil + } + for { + if err != nil { + logger.Errorf("密码登录失败: %s", err) + break + } + if ret.Successful() { + return nil + } + switch ret { + case loginstate.CaptchaVerify: + logger.Warnln("captcha verification required") + logger.Warnln(c.Sig().CaptchaURL) + aid := strings.Split(strings.Split(c.Sig().CaptchaURL, "&sid=")[1], "&")[0] + logger.Warnln("ticket?->") + ticket := utils.ReadLine() + logger.Warnln("rand_str?->") + randStr := utils.ReadLine() + ret, err = c.CommitCaptcha(ticket, randStr, aid) + continue + case loginstate.NewDeviceVerify: + vf, err := c.GetNewDeviceVerifyURL() + if err != nil { + return err + } + logger.Infoln(vf) + err = c.NewDeviceVerify(vf) + if err != nil { + return err + } + default: + logger.Errorf("Unhandled exception raised: %s", ret.Name()) + } + } + logger.Infoln("login with qrcode") + png, _, err := c.FetchQRCodeDefault() + if err != nil { + return err + } + qrcodePath := "qrcode.png" + err = os.WriteFile(qrcodePath, png, 0666) + if err != nil { + return err + } + logger.Infof("qrcode saved to %s", qrcodePath) + for { + retCode, err := c.GetQRCodeResult() + if err != nil { + logger.Errorln(err) + return err + } + if retCode.Waitable() { + time.Sleep(3 * time.Second) + continue + } + if !retCode.Success() { + return errors.New(retCode.Name()) + } + break + } + return c.QRCodeLogin() + }(qqclient) + if err != nil { - logrus.Errorln("login err:", err) + logger.Errorln("login err:", err) return } + logger.Infoln("login successed") defer qqclient.Release() defer func() { data, err = qqclient.Sig().Marshal() if err != nil { - logrus.Errorln("marshal sig.bin err:", err) + logger.Errorln("marshal sig.bin err:", err) return } err = os.WriteFile("sig.bin", data, 0644) if err != nil { - logrus.Errorln("write sig.bin err:", err) + logger.Errorln("write sig.bin err:", err) return } - logrus.Infoln("sig saved into sig.bin") + logger.Infoln("sig saved into sig.bin") }() // setup the main stop channel diff --git a/utils/crypto/rand.go b/utils/crypto/rand.go new file mode 100644 index 00000000..d97be33d --- /dev/null +++ b/utils/crypto/rand.go @@ -0,0 +1,9 @@ +package crypto + +import "crypto/rand" + +func RandomBytes(size int) []byte { + b := make([]byte, size) + _, _ = rand.Read(b) + return b +}