Add offline usage #4

Open
jolheiser wants to merge 1 commits from offline into master
5 changed files with 130 additions and 71 deletions

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
View 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
View File

@ -0,0 +1,2 @@
39b67301676bd12b620c0b5506441abd97745986:500
e65c71ea689e780d2f2264bb503ac4e2d4fc0c44:0