Add offline usage #4
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.jolheiser.com/pwn"
|
||||
|
@ -19,47 +18,30 @@ var (
|
|||
Version = "develop"
|
||||
)
|
||||
|
||||
func main() {
|
||||
paddingFlag := flag.Bool("padding", false, "Add padding to password checks - more secure, but slower")
|
||||
helpFlag := flag.Bool("help", false, "Show the help dialog")
|
||||
versionFlag := flag.Bool("version", false, "Show the version of pwn")
|
||||
flag.Parse()
|
||||
|
||||
if *helpFlag {
|
||||
help()
|
||||
os.Exit(0)
|
||||
}
|
||||
if *versionFlag {
|
||||
version()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
logError("pwn needs a sub-command")
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "help":
|
||||
help()
|
||||
case "version":
|
||||
version()
|
||||
case "password", "pw":
|
||||
checkPassword(args, *paddingFlag)
|
||||
default:
|
||||
logError("unknown sub-command")
|
||||
}
|
||||
}
|
||||
|
||||
// If used in CI of any kind
|
||||
// Exits with error code 1 if password has been pwned
|
||||
// Exits with error code 0 if password has not been pwned
|
||||
func checkPassword(args []string, padding bool) {
|
||||
if len(args) == 1 {
|
||||
logError("password must be supplied")
|
||||
func main() {
|
||||
offlineExportFlag := flag.String("offline-export", "", "Path to an offline export rather than hitting the web API")
|
||||
paddingFlag := flag.Bool("padding", false, "Add padding to password checks - more secure, but slower")
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
fmt.Println("pwn needs a password to check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
count, err := client.CheckPassword(args[1], padding)
|
||||
var count int
|
||||
var err error
|
||||
|
||||
if *offlineExportFlag != "" {
|
||||
start := time.Now()
|
||||
count, err = client.CheckPasswordOffline(args[0], *offlineExportFlag)
|
||||
fmt.Println(time.Now().Sub(start))
|
||||
} else {
|
||||
count, err = client.CheckPassword(args[0], *paddingFlag)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -70,30 +52,3 @@ func checkPassword(args []string, padding bool) {
|
|||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func logError(msg string) {
|
||||
fmt.Println(msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func help() {
|
||||
fmt.Println(strings.Trim(fmt.Sprintf(`
|
||||
pwn version %s
|
||||
|
||||
pwn is a command-line tool to test a variety of things against https://haveibeenpwned.com
|
||||
|
||||
Commands:
|
||||
help - Print this dialog
|
||||
version - Check the current version of pwn
|
||||
password, pw - Check if a password has been pwned
|
||||
|
||||
Flags:
|
||||
--help - Print this dialog
|
||||
--version - Check the current version of pwn
|
||||
--padding - Adds padding to password checks. More secure, but slower.
|
||||
`, Version), "\n"))
|
||||
}
|
||||
|
||||
func version() {
|
||||
fmt.Printf("pwn version: %s\n", Version)
|
||||
}
|
||||
|
|
55
password.go
55
password.go
|
@ -1,29 +1,28 @@
|
|||
package pwn
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const passwordURL = "https://api.pwnedpasswords.com/range/"
|
||||
|
||||
// CheckPassword returns the number of times a password has been compromised
|
||||
// CheckPassword returns the number of times a password has been compromised using the web API
|
||||
// Adding padding will make requests more secure, however is also slower
|
||||
// because artificial responses will be added to the response
|
||||
// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
||||
func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
|
||||
if strings.TrimSpace(pw) == "" {
|
||||
return -1, ErrEmptyPassword{}
|
||||
enc, err := hashPassword(pw)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
sha := sha1.New()
|
||||
sha.Write([]byte(pw))
|
||||
enc := hex.EncodeToString(sha.Sum(nil))
|
||||
prefix, suffix := enc[:5], enc[5:]
|
||||
|
||||
req, err := newRequest(c.ctx, http.MethodGet, fmt.Sprintf("%s%s", passwordURL, prefix), nil)
|
||||
|
@ -60,3 +59,45 @@ func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
|
|||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// CheckPasswordOffline returns the number of times a password has been compromised using an offline export
|
||||
func (c *Client) CheckPasswordOffline(pw, exportPath string) (int, error) {
|
||||
enc, err := hashPassword(pw)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
fi, err := os.Open(exportPath)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer fi.Close()
|
||||
|
||||
scanner := bufio.NewScanner(fi)
|
||||
for scanner.Scan() {
|
||||
if !strings.HasPrefix(scanner.Text(), enc) {
|
||||
continue
|
||||
}
|
||||
hashCount := strings.Split(scanner.Text(), ":")
|
||||
if strings.EqualFold(enc, hashCount[0]) {
|
||||
count, err := strconv.ParseInt(hashCount[1], 10, 64)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return int(count), nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func hashPassword(pw string) (string, error) {
|
||||
if strings.TrimSpace(pw) == "" {
|
||||
return "", ErrEmptyPassword{}
|
||||
}
|
||||
|
||||
sha := sha1.New()
|
||||
sha.Write([]byte(pw))
|
||||
|
||||
return hex.EncodeToString(sha.Sum(nil)), nil
|
||||
}
|
||||
|
|
|
@ -85,6 +85,32 @@ func TestPassword(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestPasswordOffline(t *testing.T) {
|
||||
tt := []struct {
|
||||
Password string
|
||||
Count int
|
||||
}{
|
||||
{"Testing1", 500},
|
||||
{"Testing2", 0},
|
||||
{"Testing3", 0}, // Not in testing data, should be 0 if not found
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Password, func(t *testing.T) {
|
||||
count, err := client.CheckPasswordOffline(tc.Password, "testdata/passwords.txt")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if count != tc.Count {
|
||||
t.Logf("incorrect count...\n\tWanted: %d\n\t Got: %d\n", tc.Count, count)
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Credit to https://golangbyexample.com/generate-random-password-golang/
|
||||
// DO NOT USE THIS FOR AN ACTUAL PASSWORD GENERATOR
|
||||
var (
|
||||
|
|
35
testdata.go
Normal file
35
testdata.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
// +build testdata
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
passwords := []struct {
|
||||
Password string
|
||||
Count int
|
||||
}{
|
||||
{"Testing1", 500},
|
||||
{"Testing2", 0},
|
||||
}
|
||||
|
||||
fi, err := os.Create("testdata/passwords.txt")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer fi.Close()
|
||||
|
||||
for _, pw := range passwords {
|
||||
sha := sha1.New()
|
||||
sha.Write([]byte(pw.Password))
|
||||
|
||||
if _, err := fi.WriteString(fmt.Sprintf("%s:%d\n", hex.EncodeToString(sha.Sum(nil)), pw.Count)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
2
testdata/passwords.txt
vendored
Normal file
2
testdata/passwords.txt
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
39b67301676bd12b620c0b5506441abd97745986:500
|
||||
e65c71ea689e780d2f2264bb503ac4e2d4fc0c44:0
|
Reference in New Issue
Block a user