Add offline usage #4
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.jolheiser.com/pwn"
|
"go.jolheiser.com/pwn"
|
||||||
|
@ -19,47 +18,30 @@ var (
|
||||||
Version = "develop"
|
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
|
// If used in CI of any kind
|
||||||
// Exits with error code 1 if password has been pwned
|
// Exits with error code 1 if password has been pwned
|
||||||
// Exits with error code 0 if password has not been pwned
|
// Exits with error code 0 if password has not been pwned
|
||||||
func checkPassword(args []string, padding bool) {
|
func main() {
|
||||||
if len(args) == 1 {
|
offlineExportFlag := flag.String("offline-export", "", "Path to an offline export rather than hitting the web API")
|
||||||
logError("password must be supplied")
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -70,30 +52,3 @@ func checkPassword(args []string, padding bool) {
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
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
|
package pwn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const passwordURL = "https://api.pwnedpasswords.com/range/"
|
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
|
// Adding padding will make requests more secure, however is also slower
|
||||||
// because artificial responses will be added to the response
|
// because artificial responses will be added to the response
|
||||||
// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
||||||
func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
|
func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
|
||||||
if strings.TrimSpace(pw) == "" {
|
enc, err := hashPassword(pw)
|
||||||
return -1, ErrEmptyPassword{}
|
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:]
|
prefix, suffix := enc[:5], enc[5:]
|
||||||
|
|
||||||
req, err := newRequest(c.ctx, http.MethodGet, fmt.Sprintf("%s%s", passwordURL, prefix), nil)
|
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
|
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/
|
// Credit to https://golangbyexample.com/generate-random-password-golang/
|
||||||
// DO NOT USE THIS FOR AN ACTUAL PASSWORD GENERATOR
|
// DO NOT USE THIS FOR AN ACTUAL PASSWORD GENERATOR
|
||||||
var (
|
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