pushbits/internal/authentication/credentials/hibp.go

66 lines
1.4 KiB
Go

package credentials
import (
"crypto/sha1" //#nosec G505 -- False positive, see the use below.
"fmt"
"io"
"net/http"
"strings"
"github.com/pushbits/server/internal/log"
)
const (
base = "https://api.pwnedpasswords.com"
pwnedHashesEndpoint = "/range"
pwnedHashesURL = base + pwnedHashesEndpoint + "/"
)
// IsPasswordPwned determines whether or not the password is weak.
func IsPasswordPwned(password string) (bool, error) {
if len(password) == 0 {
return true, nil
}
hash := sha1.Sum([]byte(password)) //#nosec G401 -- False positive, only the first 5 bytes are transmitted.
hashStr := fmt.Sprintf("%X", hash)
lookup := hashStr[0:5]
match := hashStr[5:]
log.L.Printf("Checking HIBP for hashes starting with '%s'.", lookup)
resp, err := http.Get(pwnedHashesURL + lookup)
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK {
log.L.Fatalf("Request failed with HTTP %s.", resp.Status)
}
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
log.L.Fatal(err)
}
err = resp.Body.Close()
if err != nil {
log.L.Warnf("Failed to close file: %s.", err)
}
bodyStr := string(bodyText)
lines := strings.Split(bodyStr, "\n")
for _, line := range lines {
separated := strings.Split(line, ":")
if len(separated) != 2 {
return false, fmt.Errorf("HIPB API returned malformed response: %s", line)
}
if separated[0] == match {
return true, nil
}
}
return false, nil
}