Skip to content

Commit 436bb72

Browse files
committed
refactor(doubao_new): security improvement
Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>
1 parent 0447c77 commit 436bb72

File tree

4 files changed

+159
-55
lines changed

4 files changed

+159
-55
lines changed

drivers/doubao_new/auth.go

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ type dpopKeyPairEnvelope struct {
7979
JWK *jwkECPrivateKey `json:"jwk"`
8080
}
8181

82-
const defaultDPoPKeySecret = "passport-dpop-token-generator"
83-
8482
type encryptedDpopKeyPair struct {
8583
Data string `json:"data"`
8684
Ciphertext string `json:"ciphertext"`
@@ -162,6 +160,14 @@ func GenerateDPoPToken(in DPoPTokenInput) (*DPoPTokenOutput, error) {
162160
}, nil
163161
}
164162

163+
func GenerateDPoPKeyPair() (*ecdsa.PrivateKey, error) {
164+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
165+
if err != nil {
166+
return nil, err
167+
}
168+
return validateP256Key(key)
169+
}
170+
165171
func ParseJWTPayload(token string, out any) error {
166172
token = strings.TrimSpace(trimTokenScheme(token))
167173
parts := strings.Split(token, ".")
@@ -234,7 +240,7 @@ func parseECPrivateKeyJWK(raw string) (*ecdsa.PrivateKey, error) {
234240
return validateP256Key(key)
235241
}
236242

237-
func parseEncryptedDPoPKeyPair(raw string) (*ecdsa.PrivateKey, error) {
243+
func parseEncryptedDPoPKeyPair(raw, secret string) (*ecdsa.PrivateKey, error) {
238244
raw = strings.TrimSpace(raw)
239245
if raw == "" {
240246
return nil, errors.New("empty encrypted key pair")
@@ -266,9 +272,9 @@ func parseEncryptedDPoPKeyPair(raw string) (*ecdsa.PrivateKey, error) {
266272
return nil, errors.New("encrypted dpop payload too short")
267273
}
268274

269-
plain, err := decryptDoubaoKeyPair(decoded, defaultDPoPKeySecret)
275+
plain, err := decryptDoubaoKeyPair(decoded, secret)
270276
if err != nil {
271-
return nil, fmt.Errorf("failed to decrypt with default secret: %w", err)
277+
return nil, fmt.Errorf("failed to decrypt with secret: %w", err)
272278
}
273279
return parseECPrivateKeyJWK(string(plain))
274280
}
@@ -381,7 +387,7 @@ func (d *DoubaoNew) resolveAuthorization() string {
381387
return "DPoP " + auth
382388
}
383389

384-
func shouldRefreshJWT(token string, aheadSeconds int64) bool {
390+
func shouldRefreshJWT(token string) bool {
385391
if token == "" {
386392
return true
387393
}
@@ -392,24 +398,30 @@ func shouldRefreshJWT(token string, aheadSeconds int64) bool {
392398
if payload.Exp <= 0 {
393399
return false
394400
}
395-
return payload.Exp <= time.Now().Unix()+aheadSeconds
401+
return payload.Exp <= time.Now().Unix()+defaultAuthRefreshAheadSeconds
396402
}
397403

398-
func (d *DoubaoNew) fetchBizAuth(dpop string) (string, error) {
404+
func (d *DoubaoNew) fetchBizAuth(dpop string, public bool) (string, error) {
405+
var reqUrl string
399406
client := base.RestyClient.Clone()
400407
req := client.R()
401408
req.SetHeader("accept", "application/json, text/javascript")
402409
req.SetHeader("origin", DoubaoURL)
403410
req.SetHeader("referer", DoubaoURL+"/")
404411
req.SetHeader("content-type", "application/x-www-form-urlencoded")
405-
if d.Cookie != "" {
406-
req.SetHeader("cookie", d.Cookie)
407-
if csrf := strings.TrimSpace(cookie.GetStr(d.Cookie, "passport_csrf_token")); csrf != "" {
408-
req.SetHeader("x-tt-passport-csrf-token", csrf)
412+
if public {
413+
reqUrl = DoubaoURL + "/passport/anonymity_user/biz_auth/"
414+
} else {
415+
reqUrl = DoubaoURL + "/passport/user/biz_auth/"
416+
if d.Cookie != "" {
417+
req.SetHeader("cookie", d.Cookie)
418+
if csrf := strings.TrimSpace(cookie.GetStr(d.Cookie, "passport_csrf_token")); csrf != "" {
419+
req.SetHeader("x-tt-passport-csrf-token", csrf)
420+
}
421+
}
422+
if oldAuth := d.resolveAuthorization(); oldAuth != "" {
423+
req.SetHeader("authorization", oldAuth)
409424
}
410-
}
411-
if oldAuth := d.resolveAuthorization(); oldAuth != "" {
412-
req.SetHeader("authorization", oldAuth)
413425
}
414426
if dpop != "" {
415427
req.SetHeader("dpop", dpop)
@@ -424,23 +436,22 @@ func (d *DoubaoNew) fetchBizAuth(dpop string) (string, error) {
424436
req.SetQueryParam("account_sdk_source", d.AuthSDKSource)
425437
req.SetQueryParam("sdk_version", d.AuthSDKVersion)
426438

427-
res, err := req.Post(DoubaoURL + "/passport/user/biz_auth/")
439+
res, err := req.Post(reqUrl)
428440
if err != nil {
429441
return "", err
430442
}
431443
var resp bizAuthResp
432444
if err = json.Unmarshal(res.Body(), &resp); err != nil {
433445
return "", err
434446
}
435-
ok := resp.Message != "success" && strings.TrimSpace(resp.Data.AccessToken) != ""
436-
if !ok {
447+
if resp.Message != "success" || resp.Data.AccessToken == "" {
437448
return "", fmt.Errorf("[doubao_new] %s: %s", resp.Message, resp.Data.Description)
438449
}
439-
return strings.TrimSpace(resp.Data.AccessToken), nil
450+
return resp.Data.AccessToken, nil
440451
}
441452

442453
func (d *DoubaoNew) refreshAuthorizationWithDPoP(dpop string) (string, error) {
443-
token, err := d.fetchBizAuth(dpop)
454+
token, err := d.fetchBizAuth(dpop, false)
444455
if err == nil && token != "" {
445456
return token, nil
446457
}
@@ -451,9 +462,9 @@ func (d *DoubaoNew) refreshAuthorizationWithDPoP(dpop string) (string, error) {
451462
}
452463

453464
func (d *DoubaoNew) resolveDpopForRequest(method, rawURL string) (string, error) {
454-
if d.DpopKeyPair != nil {
465+
if d.DPoPKeyPair != nil {
455466
proof, err := GenerateDPoPToken(DPoPTokenInput{
456-
KeyPair: d.DpopKeyPair,
467+
KeyPair: d.DPoPKeyPair,
457468
HTM: strings.ToUpper(strings.TrimSpace(method)),
458469
HTU: normalizeDPoPURL(rawURL),
459470
})
@@ -463,37 +474,39 @@ func (d *DoubaoNew) resolveDpopForRequest(method, rawURL string) (string, error)
463474
return proof.DPoPToken, nil
464475
}
465476

466-
static := d.Dpop
477+
static := d.DPoP
467478
if static == "" {
468479
return "", nil
469480
}
470-
if payload, err := parseDPoPPayload(static); err == nil && payload.Exp > 0 {
471-
now := time.Now().Unix()
472-
if payload.Exp <= now+defaultDpopRefreshAheadSeconds {
473-
return "", errors.New("static dpop token expired or near expiry; configure dpop_key_pair for automatic refresh")
481+
if !d.IgnoreJWTCheck {
482+
if payload, err := parseDPoPPayload(static); err == nil && payload.Exp > 0 {
483+
now := time.Now().Unix()
484+
if payload.Exp <= now+defaultDpopRefreshAheadSeconds {
485+
return "", errors.New("static dpop token expired or near expiry; configure dpop_key_pair for automatic refresh")
486+
}
474487
}
475488
}
476489
return static, nil
477490
}
478491

492+
func (d *DoubaoNew) ensureAuthAdditons() bool {
493+
return d.DPoPKeySecret != "" && d.AuthClientID != "" && d.AuthClientType != "" &&
494+
d.AuthScope != "" && d.AuthSDKSource != "" && d.AuthSDKVersion != ""
495+
}
496+
479497
func (d *DoubaoNew) resolveAuthorizationForRequest(method, rawURL string) (string, error) {
480-
if !shouldRefreshJWT(d.Authorization, defaultAuthRefreshAheadSeconds) {
498+
if !shouldRefreshJWT(d.Authorization) {
481499
return d.resolveAuthorization(), nil
482500
}
483-
// 刷新 Authorization Token 的前置条件:
484-
// 1. DPoP 密钥对
485-
// 2. Cookie
486-
// 3. AuthClientID、AuthClientType、AuthScope、AuthSDKSource、AuthSDKVersion
487-
if d.DpopKeyPair == nil || strings.TrimSpace(d.Cookie) == "" ||
488-
d.AuthClientID == "" || d.AuthClientType == "" || d.AuthScope == "" ||
489-
d.AuthSDKSource == "" || d.AuthSDKVersion == "" {
501+
502+
if d.DPoPKeyPair == nil || strings.TrimSpace(d.Cookie) == "" || !d.ensureAuthAdditons() {
490503
return d.resolveAuthorization(), nil
491504
}
492505

493506
d.authRefreshMu.Lock()
494507
defer d.authRefreshMu.Unlock()
495508

496-
if !shouldRefreshJWT(d.Authorization, defaultAuthRefreshAheadSeconds) {
509+
if !shouldRefreshJWT(d.Authorization) {
497510
return d.resolveAuthorization(), nil
498511
}
499512

@@ -513,6 +526,41 @@ func (d *DoubaoNew) resolveAuthorizationForRequest(method, rawURL string) (strin
513526
return d.resolveAuthorization(), nil
514527
}
515528

529+
func (d *DoubaoNew) resolveAuthorizationForPublic() (dpop string, auth string, err error) {
530+
if d.DPoPPublic != "" && !shouldRefreshJWT(d.AuthorizationPublic) {
531+
return d.DPoPPublic, "DPoP " + d.AuthorizationPublic, nil
532+
}
533+
534+
if !d.ensureAuthAdditons() {
535+
return "", "", fmt.Errorf("[doubao_new] missing auth additions, please fill them all")
536+
}
537+
538+
d.authRefreshPublicMu.Lock()
539+
defer d.authRefreshPublicMu.Unlock()
540+
541+
if d.DPoPPublic != "" && !shouldRefreshJWT(d.AuthorizationPublic) {
542+
return d.DPoPPublic, "DPoP " + d.AuthorizationPublic, nil
543+
}
544+
545+
// generate new public dpop
546+
keypair, err := GenerateDPoPKeyPair()
547+
if err != nil {
548+
return "", "", err
549+
}
550+
proof, err := GenerateDPoPToken(DPoPTokenInput{
551+
KeyPair: keypair,
552+
})
553+
d.DPoPPublic = proof.DPoPToken
554+
555+
// get authorization token
556+
d.AuthorizationPublic, err = d.fetchBizAuth(proof.DPoPToken, true)
557+
if err != nil {
558+
return "", "", err
559+
}
560+
561+
return d.DPoPPublic, "DPoP " + d.AuthorizationPublic, nil
562+
}
563+
516564
func (d *DoubaoNew) applyAuthHeaders(req *resty.Request, method, rawURL string) error {
517565
auth, err := d.resolveAuthorizationForRequest(method, rawURL)
518566
if err != nil {

drivers/doubao_new/driver.go

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,18 @@ type DoubaoNew struct {
3131
Addition
3232
TtLogid string
3333

34-
// DPoP access token (Authorization header value)
35-
Authorization string
34+
// DPoP access token (Authorization header value, without DPoP prefix)
35+
Authorization string
36+
AuthorizationPublic string
3637
// DPoP header value
37-
Dpop string
38+
DPoP string
39+
DPoPPublic string
3840
// DPoP key pair for generating DPoP
39-
DpopKeyPairStr string
40-
DpopKeyPair *ecdsa.PrivateKey
41+
DPoPKeyPairStr string
42+
DPoPKeyPair *ecdsa.PrivateKey
4143

42-
authRefreshMu sync.Mutex
44+
authRefreshMu sync.Mutex
45+
authRefreshPublicMu sync.Mutex
4346
}
4447

4548
func (d *DoubaoNew) Config() driver.Config {
@@ -59,12 +62,12 @@ func (d *DoubaoNew) Init(ctx context.Context) error {
5962
}
6063
dpop := strings.TrimSpace(cookie.GetStr(d.Cookie, "LARK_SUITE_DPOP"))
6164
if dpop != "" {
62-
d.Dpop = dpop
65+
d.DPoP = dpop
6366
}
6467
keypair := strings.TrimSpace(cookie.GetStr(d.Cookie, "feishu_dpop_keypair"))
65-
if keypair != "" {
66-
d.DpopKeyPairStr = keypair
67-
d.DpopKeyPair, _ = parseEncryptedDPoPKeyPair(keypair)
68+
if keypair != "" && d.DPoPKeySecret != "" {
69+
d.DPoPKeyPairStr = keypair
70+
d.DPoPKeyPair, _ = parseEncryptedDPoPKeyPair(keypair, d.DPoPKeySecret)
6871
}
6972
}
7073
return nil
@@ -74,11 +77,11 @@ func (d *DoubaoNew) Drop(ctx context.Context) error {
7477
if d.Authorization != "" {
7578
d.Cookie = cookie.SetStr(d.Cookie, "LARK_SUITE_ACCESS_TOKEN", d.Authorization)
7679
}
77-
if d.Dpop != "" {
78-
d.Cookie = cookie.SetStr(d.Cookie, "LARK_SUITE_DPOP", d.Dpop)
80+
if d.DPoP != "" {
81+
d.Cookie = cookie.SetStr(d.Cookie, "LARK_SUITE_DPOP", d.DPoP)
7982
}
80-
if d.DpopKeyPairStr != "" {
81-
d.Cookie = cookie.SetStr(d.Cookie, "feishu_dpop_keypair", d.DpopKeyPairStr)
83+
if d.DPoPKeyPairStr != "" {
84+
d.Cookie = cookie.SetStr(d.Cookie, "feishu_dpop_keypair", d.DPoPKeyPairStr)
8285
}
8386
op.MustSaveDriverStorage(d)
8487
return nil
@@ -131,13 +134,26 @@ func (d *DoubaoNew) Link(ctx context.Context, file model.Obj, args model.LinkArg
131134
if obj.IsFolder {
132135
return nil, fmt.Errorf("link is directory")
133136
}
134-
if args.Type == "preview" || args.Type == "thumb" {
135-
if link, err := d.previewLink(ctx, obj, args); err == nil {
136-
return link, nil
137+
var (
138+
err error
139+
auth, dpop string
140+
)
141+
if d.ShareLink {
142+
err := d.createShare(ctx, obj)
143+
if err != nil {
144+
return nil, err
145+
}
146+
dpop, auth, err = d.resolveAuthorizationForPublic()
147+
} else {
148+
// TODO: append previewLink() with auth args to support ShareLink
149+
if args.Type == "preview" || args.Type == "thumb" {
150+
if link, err := d.previewLink(ctx, obj, args); err == nil {
151+
return link, nil
152+
}
137153
}
154+
auth = d.resolveAuthorization()
155+
dpop, err = d.resolveDpopForRequest(http.MethodGet, DownloadBaseURL+"/space/api/box/stream/download/all/"+obj.ObjToken+"/")
138156
}
139-
auth := d.resolveAuthorization()
140-
dpop, err := d.resolveDpopForRequest(http.MethodGet, DownloadBaseURL+"/space/api/box/stream/download/all/"+obj.ObjToken+"/")
141157
if err != nil {
142158
return nil, err
143159
}

drivers/doubao_new/meta.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ type Addition struct {
1111
// define other
1212
Cookie string `json:"cookie" required:"true" help:"Web Cookie"`
1313
AppID string `json:"app_id" required:"true" default:"497858" help:"Doubao App ID"`
14+
DPoPKeySecret string `json:"dpop_key_secret" help:"DPoP Key Secret for generating DPoP token"`
1415
AuthClientID string `json:"auth_client_id" help:"Doubao Biz Auth Client ID"`
1516
AuthClientType string `json:"auth_client_type" help:"Doubao Biz Auth Client Type"`
1617
AuthScope string `json:"auth_scope" help:"Doubao Biz Auth Scope"`
1718
AuthSDKSource string `json:"auth_sdk_source" help:"Doubao Biz Auth SDK Source"`
1819
AuthSDKVersion string `json:"auth_sdk_version" help:"Doubao Biz Auth SDK Version"`
20+
ShareLink bool `json:"share_link" help:"Whether to use share link for download"`
21+
IgnoreJWTCheck bool `json:"ignore_jwt_check" help:"Whether to ignore JWT check to prevent time issue"`
1922
}
2023

2124
var config = driver.Config{

drivers/doubao_new/util.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,43 @@ func (d *DoubaoNew) previewLink(ctx context.Context, obj *Object, args model.Lin
267267
}, nil
268268
}
269269

270+
func (d *DoubaoNew) createShare(ctx context.Context, obj *Object) error {
271+
doRequest := func(csrfToken string) (*resty.Response, []byte, error) {
272+
req := base.RestyClient.R()
273+
req.SetContext(ctx)
274+
req.SetHeader("accept", "application/json, text/plain, */*")
275+
req.SetHeader("origin", DoubaoURL)
276+
req.SetHeader("referer", DoubaoURL+"/")
277+
if err := d.applyAuthHeaders(req, http.MethodPost, BaseURL+"/space/api/suite/permission/public/update.v5/"); err != nil {
278+
return nil, nil, err
279+
}
280+
if csrfToken != "" {
281+
req.SetHeader("x-csrftoken", csrfToken)
282+
}
283+
req.SetHeader("Content-Type", "application/json")
284+
req.SetBody(base.Json{
285+
"external_access_entity": 1,
286+
"link_share_entity": 4,
287+
"token": obj.ObjToken,
288+
"type": obj.ObjType,
289+
})
290+
res, err := req.Execute(http.MethodPost, BaseURL+"/space/api/suite/permission/public/update.v5/")
291+
if err != nil {
292+
return nil, nil, err
293+
}
294+
return res, res.Body(), nil
295+
}
296+
297+
res, body, err := doRequestWithCsrf(doRequest)
298+
if err != nil {
299+
return err
300+
}
301+
if err := decodeBaseResp(body, res); err != nil {
302+
return err
303+
}
304+
return nil
305+
}
306+
270307
func (d *DoubaoNew) createFolder(ctx context.Context, parentToken, name string) (Node, error) {
271308
data := url.Values{}
272309
data.Set("name", name)

0 commit comments

Comments
 (0)