safing-jess/cmd/password.go
2022-02-02 15:21:58 +01:00

155 lines
3.7 KiB
Go

package main
import (
"bufio"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"log"
"net/http"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/safing/jess"
)
func registerPasswordCallbacks() {
jess.SetPasswordCallbacks(createPasswordInterface, getPasswordInterface)
}
func getPasswordInterface(signet *jess.Signet) error {
pw, err := getPassword(formatSignetName(signet))
if err != nil {
return err
}
signet.Key = []byte(pw)
return nil
}
func createPasswordInterface(signet *jess.Signet, minSecurityLevel int) error {
pw, err := createPassword(formatSignetName(signet), minSecurityLevel)
if err != nil {
return err
}
signet.Key = []byte(pw)
return nil
}
func getPassword(reference string) (string, error) {
// enter new pw
var pw string
prompt := &survey.Password{
Message: makePrompt("Please enter password", reference),
}
err := survey.AskOne(prompt, &pw, nil)
if err != nil {
return "", err
}
return pw, nil
}
func createPassword(reference string, minSecurityLevel int) (string, error) {
// enter new pw
var pw1 string
prompt := &survey.Password{
Message: makePrompt("Please enter password", reference),
}
err := survey.AskOne(prompt, &pw1, survey.WithValidator(func(val interface{}) error {
pwVal, ok := val.(string)
if !ok {
return errors.New("input error")
}
// TODO: adapt interations based on tool
pwSecLevel := jess.CalculatePasswordSecurityLevel(pwVal, 20000)
if pwSecLevel < minSecurityLevel {
return fmt.Errorf("please enter a stronger password, you only reached %d bits of security, while the envelope has a minimum of %d", pwSecLevel, minSecurityLevel)
}
return nil
}))
if err != nil {
return "", err
}
// confirm
var pw2 string
prompt = &survey.Password{
Message: makePrompt("Please confirm password", reference),
}
err = survey.AskOne(prompt, &pw2, nil)
if err != nil {
return "", err
}
// check match
if pw1 != pw2 {
return "", errors.New("the entered passwords mismatch")
}
// check password?
check, err := confirm("Do you want to check if the password has been compromised in the past?", false)
if err != nil {
return "", err
}
if check {
err := checkForWeakPassword(pw1)
if err != nil {
return "", err
}
}
return pw1, nil
}
func checkForWeakPassword(pw string) error {
// check HIBP
// docs: https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange
// hash and split
sum := sha1.Sum([]byte(pw)) //nolint:gosec // required for HIBP API
hexSum := hex.EncodeToString(sum[:])
prefix := strings.ToUpper(hexSum[:5])
suffix := strings.ToUpper(hexSum[5:])
// request hash list
resp, err := http.Get(fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", prefix))
if err != nil {
return fmt.Errorf("failed to contact HIBP service: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
// check if password is in hash list
bodyReader := bufio.NewReader(resp.Body)
scanner := bufio.NewScanner(bodyReader)
cnt := 0
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), suffix) {
log.Printf("%+v", scanner.Text())
fields := strings.Split(scanner.Text(), ":")
log.Printf("%+v", fields)
if len(fields) >= 2 {
//nolint:golint,stylecheck // is user error message
return fmt.Errorf("password detected in HIBP database - it has been leaked %s times!", fields[1])
}
//nolint:golint,stylecheck // is user error message
return errors.New("password detected in HIBP database - it has been leaked!")
}
cnt++
}
// fmt.Printf("checked %d leaked passwords\n", cnt)
if err := scanner.Err(); err != nil {
return fmt.Errorf("failed to read HIBP response: %w", err)
}
return nil
}
func makePrompt(prompt, reference string) string {
if reference != "" {
return fmt.Sprintf(`%s "%s":`, prompt, reference)
}
return fmt.Sprintf("%s:", prompt)
}