code.gitea.io/gitea@v1.19.3/modules/auth/password/pwn/pwn.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package pwn 5 6 import ( 7 "context" 8 "crypto/sha1" 9 "encoding/hex" 10 "errors" 11 "fmt" 12 "io" 13 "net/http" 14 "strconv" 15 "strings" 16 17 "code.gitea.io/gitea/modules/setting" 18 ) 19 20 const passwordURL = "https://api.pwnedpasswords.com/range/" 21 22 // ErrEmptyPassword is an empty password error 23 var ErrEmptyPassword = errors.New("password cannot be empty") 24 25 // Client is a HaveIBeenPwned client 26 type Client struct { 27 ctx context.Context 28 http *http.Client 29 } 30 31 // New returns a new HaveIBeenPwned Client 32 func New(options ...ClientOption) *Client { 33 client := &Client{ 34 ctx: context.Background(), 35 http: http.DefaultClient, 36 } 37 38 for _, opt := range options { 39 opt(client) 40 } 41 42 return client 43 } 44 45 // ClientOption is a way to modify a new Client 46 type ClientOption func(*Client) 47 48 // WithHTTP will set the http.Client of a Client 49 func WithHTTP(httpClient *http.Client) func(pwnClient *Client) { 50 return func(pwnClient *Client) { 51 pwnClient.http = httpClient 52 } 53 } 54 55 // WithContext will set the context.Context of a Client 56 func WithContext(ctx context.Context) func(pwnClient *Client) { 57 return func(pwnClient *Client) { 58 pwnClient.ctx = ctx 59 } 60 } 61 62 func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*http.Request, error) { 63 req, err := http.NewRequestWithContext(ctx, method, url, body) 64 if err != nil { 65 return nil, err 66 } 67 req.Header.Add("User-Agent", "Gitea "+setting.AppVer) 68 return req, nil 69 } 70 71 // CheckPassword returns the number of times a password has been compromised 72 // Adding padding will make requests more secure, however is also slower 73 // because artificial responses will be added to the response 74 // For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ 75 func (c *Client) CheckPassword(pw string, padding bool) (int, error) { 76 if strings.TrimSpace(pw) == "" { 77 return -1, ErrEmptyPassword 78 } 79 80 sha := sha1.New() 81 sha.Write([]byte(pw)) 82 enc := hex.EncodeToString(sha.Sum(nil)) 83 prefix, suffix := enc[:5], enc[5:] 84 85 req, err := newRequest(c.ctx, http.MethodGet, fmt.Sprintf("%s%s", passwordURL, prefix), nil) 86 if err != nil { 87 return -1, nil 88 } 89 if padding { 90 req.Header.Add("Add-Padding", "true") 91 } 92 93 resp, err := c.http.Do(req) 94 if err != nil { 95 return -1, err 96 } 97 98 body, err := io.ReadAll(resp.Body) 99 if err != nil { 100 return -1, err 101 } 102 defer resp.Body.Close() 103 104 for _, pair := range strings.Split(string(body), "\n") { 105 parts := strings.Split(pair, ":") 106 if len(parts) != 2 { 107 continue 108 } 109 if strings.EqualFold(suffix, parts[0]) { 110 count, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64) 111 if err != nil { 112 return -1, err 113 } 114 return int(count), nil 115 } 116 } 117 return 0, nil 118 }