diff --git a/cmdGet.go b/cmdGet.go index df3313a..096519e 100644 --- a/cmdGet.go +++ b/cmdGet.go @@ -5,9 +5,10 @@ import ( "fmt" "os" + "github.com/spf13/cobra" + "github.com/Luzifer/password/hasher" pwd "github.com/Luzifer/password/lib" - "github.com/spf13/cobra" ) func getCmdGet() *cobra.Command { @@ -25,6 +26,8 @@ func getCmdGet() *cobra.Command { cmd.Flags().BoolVarP(&flags.CLI.XKCD, "xkcd", "x", false, "use XKCD style password") cmd.Flags().BoolVarP(&flags.CLI.PrependDate, "date", "d", true, "prepend current date to XKCD style passwords") + cmd.Flags().Bool("check-hibp", false, "Check HaveIBeenPwned for this password") + return &cmd } @@ -36,12 +39,25 @@ func actionCmdGet(cmd *cobra.Command, args []string) { for i := 0; i < flags.CLI.Num; i++ { + regenerate: if flags.CLI.XKCD { password, err = pwd.DefaultXKCD.GeneratePassword(flags.CLI.Length, flags.CLI.PrependDate) } else { password, err = pwd.NewSecurePassword().GeneratePassword(flags.CLI.Length, flags.CLI.SpecialCharacters) } + if c, _ := cmd.Flags().GetBool("check-hibp"); c { + switch pwd.CheckHIBPPasswordHash(password) { + case pwd.ErrPasswordInBreach: + goto regenerate + case nil: + // Just do nothing + default: + fmt.Printf("Unable to check for password pwnage: %s", err) + os.Exit(1) + } + } + if err != nil { switch { case err == pwd.ErrLengthTooLow: diff --git a/lib/hibp.go b/lib/hibp.go new file mode 100644 index 0000000..919a0eb --- /dev/null +++ b/lib/hibp.go @@ -0,0 +1,49 @@ +package securepassword + +import ( + "bufio" + "crypto/sha1" + "fmt" + "net/http" + "strings" + + "github.com/pkg/errors" +) + +// ErrPasswordInBreach signals the password passed was found in any +// breach at least once. The password should not be used if this +// error is returned. +var ErrPasswordInBreach = errors.New("Given password is known to HaveIBeenPwned") + +// CheckHIBPPasswordHash accesses the HaveIBeenPwned API with the +// first 5 characters of the SHA1 hash of the password and scans the +// result for the password hash. If the hash is found the +// ErrPasswordInBreach error is thrown. In case of an HTTP error +// another error is thrown. The result will be nil when the password +// hash was not returned in the API output. +// +// See more details at https://haveibeenpwned.com/API/v2#PwnedPasswords +func CheckHIBPPasswordHash(password string) error { + fullHash := fmt.Sprintf("%x", sha1.Sum([]byte(password))) + checkHash := fullHash[0:5] + + resp, err := http.Get("https://api.pwnedpasswords.com/range/" + checkHash) + if err != nil { + return errors.Wrap(err, "HTTP request failed") + } + defer resp.Body.Close() + + // Response format: + // 0018A45C4D1DEF81644B54AB7F969B88D65:1 + // 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2 + + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), fullHash) { + // We don't care for the exact number but only for a match + return ErrPasswordInBreach + } + } + + return nil +}