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  }