mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-09-11 14:32:48 +02:00
Merge pull request #28 from wneessen/27-add-ntlm-hash-support
#27: Implement NTLM hash support for PwnedPassAPI
This commit is contained in:
commit
2e1355761c
8 changed files with 636 additions and 12 deletions
32
hibp.go
32
hibp.go
|
@ -49,8 +49,18 @@ var (
|
|||
// expected length
|
||||
ErrSHA1LengthMismatch = errors.New("SHA1 hash size needs to be 160 bits")
|
||||
|
||||
// ErrNTLMLengthMismatch should be used if a given NTLM hash does not match the
|
||||
// expected length
|
||||
ErrNTLMLengthMismatch = errors.New("NTLM hash size needs to be 128 bits")
|
||||
|
||||
// ErrSHA1Invalid should be used if a given string does not represent a valid SHA1 hash
|
||||
ErrSHA1Invalid = errors.New("not a valid SHA1 hash")
|
||||
|
||||
// ErrNTLMInvalid should be used if a given string does not represent a valid NTLM hash
|
||||
ErrNTLMInvalid = errors.New("not a valid NTLM hash")
|
||||
|
||||
// ErrUnsupportedHashMode should be used if a given hash mode is not supported
|
||||
ErrUnsupportedHashMode = errors.New("hash mode not supported")
|
||||
)
|
||||
|
||||
// Client is the HIBP client object
|
||||
|
@ -80,7 +90,10 @@ func New(options ...Option) Client {
|
|||
|
||||
// Set defaults
|
||||
c.to = DefaultTimeout
|
||||
c.PwnedPassAPIOpts = &PwnedPasswordOptions{}
|
||||
c.PwnedPassAPIOpts = &PwnedPasswordOptions{
|
||||
HashMode: HashModeSHA1,
|
||||
WithPadding: false,
|
||||
}
|
||||
c.ua = DefaultUserAgent
|
||||
|
||||
// Set additional options
|
||||
|
@ -95,7 +108,10 @@ func New(options ...Option) Client {
|
|||
c.hc = httpClient(c.to)
|
||||
|
||||
// Associate the different HIBP service APIs with the Client
|
||||
c.PwnedPassAPI = &PwnedPassAPI{hibp: &c}
|
||||
c.PwnedPassAPI = &PwnedPassAPI{
|
||||
hibp: &c,
|
||||
ParamMap: make(map[string]string),
|
||||
}
|
||||
c.BreachAPI = &BreachAPI{hibp: &c}
|
||||
c.PasteAPI = &PasteAPI{hibp: &c}
|
||||
|
||||
|
@ -140,6 +156,18 @@ func WithRateLimitSleep() Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithPwnedNTLMHash sets the hash mode for the PwnedPasswords API to NTLM hashes
|
||||
//
|
||||
// Note: This option only affects the generic methods like PwnedPassAPI.CheckPassword
|
||||
// or PwnedPassAPI.ListHashesPassword. For any specifc method with the hash type in
|
||||
// the method name, this option is ignored and the hash type of the function is
|
||||
// forced
|
||||
func WithPwnedNTLMHash() Option {
|
||||
return func(c *Client) {
|
||||
c.PwnedPassAPIOpts.HashMode = HashModeNTLM
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPReq performs an HTTP request to the corresponding API
|
||||
func (c *Client) HTTPReq(m, p string, q map[string]string) (*http.Request, error) {
|
||||
u, err := url.Parse(p)
|
||||
|
|
16
hibp_test.go
16
hibp_test.go
|
@ -37,11 +37,25 @@ func TestNewWithHttpTimeout(t *testing.T) {
|
|||
func TestNewWithPwnedPadding(t *testing.T) {
|
||||
hc := New(WithPwnedPadding())
|
||||
if !hc.PwnedPassAPIOpts.WithPadding {
|
||||
t.Errorf("hibp client pwned padding option was not set properly. Expected %v, got: %v",
|
||||
t.Errorf("hibp client pwned padding option was not set properly. Expected %t, got: %t",
|
||||
true, hc.PwnedPassAPIOpts.WithPadding)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewWithPwnedNTLMHash tests the New() function with the PwnedPadding option
|
||||
func TestNewWithPwnedNTLMHash(t *testing.T) {
|
||||
hc := New(WithPwnedNTLMHash())
|
||||
if hc.PwnedPassAPIOpts.HashMode != HashModeNTLM {
|
||||
t.Errorf("hibp client NTLM hash mode option was not set properly. Expected %d, got: %d",
|
||||
HashModeNTLM, hc.PwnedPassAPIOpts.HashMode)
|
||||
}
|
||||
hc = New()
|
||||
if hc.PwnedPassAPIOpts.HashMode != HashModeSHA1 {
|
||||
t.Errorf("hibp client SHA-1 hash mode option was not set properly. Expected %d, got: %d",
|
||||
HashModeSHA1, hc.PwnedPassAPIOpts.HashMode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewWithApiKey tests the New() function with the API key set
|
||||
func TestNewWithApiKey(t *testing.T) {
|
||||
apiKey := os.Getenv("HIBP_API_KEY")
|
||||
|
|
27
md4/LICENSE
Normal file
27
md4/LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
122
md4/md4.go
Normal file
122
md4/md4.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package md4 implements the MD4 hash algorithm as defined in RFC 1320.
|
||||
//
|
||||
// NOTE: MD4 is cryptographically broken and should should only be used
|
||||
// where compatibility with legacy systems, not security, is the goal. Instead,
|
||||
// use a secure hash like SHA-256 (from crypto/sha256).
|
||||
package md4 // import "golang.org/x/crypto/md4"
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"hash"
|
||||
)
|
||||
|
||||
func init() {
|
||||
crypto.RegisterHash(crypto.MD4, New)
|
||||
}
|
||||
|
||||
// Size is the size of an MD4 checksum in bytes.
|
||||
const Size = 16
|
||||
|
||||
// BlockSize is the blocksize of MD4 in bytes.
|
||||
const BlockSize = 64
|
||||
|
||||
const (
|
||||
_Chunk = 64
|
||||
_Init0 = 0x67452301
|
||||
_Init1 = 0xEFCDAB89
|
||||
_Init2 = 0x98BADCFE
|
||||
_Init3 = 0x10325476
|
||||
)
|
||||
|
||||
// digest represents the partial evaluation of a checksum.
|
||||
type digest struct {
|
||||
s [4]uint32
|
||||
x [_Chunk]byte
|
||||
nx int
|
||||
len uint64
|
||||
}
|
||||
|
||||
func (d *digest) Reset() {
|
||||
d.s[0] = _Init0
|
||||
d.s[1] = _Init1
|
||||
d.s[2] = _Init2
|
||||
d.s[3] = _Init3
|
||||
d.nx = 0
|
||||
d.len = 0
|
||||
}
|
||||
|
||||
// New returns a new hash.Hash computing the MD4 checksum.
|
||||
func New() hash.Hash {
|
||||
d := new(digest)
|
||||
d.Reset()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *digest) Size() int { return Size }
|
||||
|
||||
func (d *digest) BlockSize() int { return BlockSize }
|
||||
|
||||
func (d *digest) Write(p []byte) (nn int, err error) {
|
||||
nn = len(p)
|
||||
d.len += uint64(nn)
|
||||
if d.nx > 0 {
|
||||
n := len(p)
|
||||
if n > _Chunk-d.nx {
|
||||
n = _Chunk - d.nx
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
d.x[d.nx+i] = p[i]
|
||||
}
|
||||
d.nx += n
|
||||
if d.nx == _Chunk {
|
||||
_Block(d, d.x[0:])
|
||||
d.nx = 0
|
||||
}
|
||||
p = p[n:]
|
||||
}
|
||||
n := _Block(d, p)
|
||||
p = p[n:]
|
||||
if len(p) > 0 {
|
||||
d.nx = copy(d.x[:], p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *digest) Sum(in []byte) []byte {
|
||||
// Make a copy of d0, so that caller can keep writing and summing.
|
||||
dc := new(digest)
|
||||
*dc = *d
|
||||
|
||||
// Padding. Add a 1 bit and 0 bits until 56 bytes mod 64.
|
||||
plen := dc.len
|
||||
var tmp [64]byte
|
||||
tmp[0] = 0x80
|
||||
if plen%64 < 56 {
|
||||
_, _ = dc.Write(tmp[0 : 56-plen%64])
|
||||
} else {
|
||||
_, _ = dc.Write(tmp[0 : 64+56-plen%64])
|
||||
}
|
||||
|
||||
// Length in bits.
|
||||
plen <<= 3
|
||||
for i := uint(0); i < 8; i++ {
|
||||
tmp[i] = byte(plen >> (8 * i))
|
||||
}
|
||||
_, _ = dc.Write(tmp[0:8])
|
||||
|
||||
if dc.nx != 0 {
|
||||
panic("dc.nx != 0")
|
||||
}
|
||||
|
||||
for _, s := range dc.s {
|
||||
in = append(in, byte(s>>0))
|
||||
in = append(in, byte(s>>8))
|
||||
in = append(in, byte(s>>16))
|
||||
in = append(in, byte(s>>24))
|
||||
}
|
||||
return in
|
||||
}
|
71
md4/md4_test.go
Normal file
71
md4/md4_test.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package md4
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type md4Test struct {
|
||||
out string
|
||||
in string
|
||||
}
|
||||
|
||||
var golden = []md4Test{
|
||||
{"31d6cfe0d16ae931b73c59d7e0c089c0", ""},
|
||||
{"bde52cb31de33e46245e05fbdbd6fb24", "a"},
|
||||
{"ec388dd78999dfc7cf4632465693b6bf", "ab"},
|
||||
{"a448017aaf21d8525fc10ae87aa6729d", "abc"},
|
||||
{"41decd8f579255c5200f86a4bb3ba740", "abcd"},
|
||||
{"9803f4a34e8eb14f96adba49064a0c41", "abcde"},
|
||||
{"804e7f1c2586e50b49ac65db5b645131", "abcdef"},
|
||||
{"752f4adfe53d1da0241b5bc216d098fc", "abcdefg"},
|
||||
{"ad9daf8d49d81988590a6f0e745d15dd", "abcdefgh"},
|
||||
{"1e4e28b05464316b56402b3815ed2dfd", "abcdefghi"},
|
||||
{"dc959c6f5d6f9e04e4380777cc964b3d", "abcdefghij"},
|
||||
{"1b5701e265778898ef7de5623bbe7cc0", "Discard medicine more than two years old."},
|
||||
{"d7f087e090fe7ad4a01cb59dacc9a572", "He who has a shady past knows that nice guys finish last."},
|
||||
{"a6f8fd6df617c72837592fc3570595c9", "I wouldn't marry him with a ten foot pole."},
|
||||
{"c92a84a9526da8abc240c05d6b1a1ce0", "Free! Free!/A trip/to Mars/for 900/empty jars/Burma Shave"},
|
||||
{"f6013160c4dcb00847069fee3bb09803", "The days of the digital watch are numbered. -Tom Stoppard"},
|
||||
{"2c3bb64f50b9107ed57640fe94bec09f", "Nepal premier won't resign."},
|
||||
{"45b7d8a32c7806f2f7f897332774d6e4", "For every action there is an equal and opposite government program."},
|
||||
{"b5b4f9026b175c62d7654bdc3a1cd438", "His money is twice tainted: 'taint yours and 'taint mine."},
|
||||
{"caf44e80f2c20ce19b5ba1cab766e7bd", "There is no reason for any individual to have a computer in their home. -Ken Olsen, 1977"},
|
||||
{"191fae6707f496aa54a6bce9f2ecf74d", "It's a tiny change to the code and not completely disgusting. - Bob Manchek"},
|
||||
{"9ddc753e7a4ccee6081cd1b45b23a834", "size: a.out: bad magic"},
|
||||
{"8d050f55b1cadb9323474564be08a521", "The major problem is with sendmail. -Mark Horton"},
|
||||
{"ad6e2587f74c3e3cc19146f6127fa2e3", "Give me a rock, paper and scissors and I will move the world. CCFestoon"},
|
||||
{"1d616d60a5fabe85589c3f1566ca7fca", "If the enemy is within range, then so are you."},
|
||||
{"aec3326a4f496a2ced65a1963f84577f", "It's well we cannot hear the screams/That we create in others' dreams."},
|
||||
{"77b4fd762d6b9245e61c50bf6ebf118b", "You remind me of a TV show, but that's all right: I watch it anyway."},
|
||||
{"e8f48c726bae5e516f6ddb1a4fe62438", "C is as portable as Stonehedge!!"},
|
||||
{"a3a84366e7219e887423b01f9be7166e", "Even if I could be Shakespeare, I think I should still choose to be Faraday. - A. Huxley"},
|
||||
{"a6b7aa35157e984ef5d9b7f32e5fbb52", "The fugacity of a constituent in a mixture of gases at a given temperature is proportional to its mole fraction. Lewis-Randall Rule"},
|
||||
{"75661f0545955f8f9abeeb17845f3fd6", "How can you write a big system without C++? -Paul Glick"},
|
||||
}
|
||||
|
||||
func TestGolden(t *testing.T) {
|
||||
for i := 0; i < len(golden); i++ {
|
||||
g := golden[i]
|
||||
c := New()
|
||||
for j := 0; j < 3; j++ {
|
||||
if j < 2 {
|
||||
_, _ = io.WriteString(c, g.in)
|
||||
} else {
|
||||
_, _ = io.WriteString(c, g.in[0:len(g.in)/2])
|
||||
c.Sum(nil)
|
||||
_, _ = io.WriteString(c, g.in[len(g.in)/2:])
|
||||
}
|
||||
s := fmt.Sprintf("%x", c.Sum(nil))
|
||||
if s != g.out {
|
||||
t.Fatalf("md4[%d](%s) = %s want %s", j, g.in, s, g.out)
|
||||
}
|
||||
c.Reset()
|
||||
}
|
||||
}
|
||||
}
|
95
md4/md4block.go
Normal file
95
md4/md4block.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// MD4 block step.
|
||||
// In its own file so that a faster assembly or C version
|
||||
// can be substituted easily.
|
||||
|
||||
package md4
|
||||
|
||||
import "math/bits"
|
||||
|
||||
var (
|
||||
shift1 = []int{3, 7, 11, 19}
|
||||
shift2 = []int{3, 5, 9, 13}
|
||||
shift3 = []int{3, 9, 11, 15}
|
||||
)
|
||||
|
||||
var (
|
||||
xIndex2 = []uint{0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15}
|
||||
xIndex3 = []uint{0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}
|
||||
)
|
||||
|
||||
func _Block(dig *digest, p []byte) int {
|
||||
a := dig.s[0]
|
||||
b := dig.s[1]
|
||||
c := dig.s[2]
|
||||
d := dig.s[3]
|
||||
n := 0
|
||||
var X [16]uint32
|
||||
for len(p) >= _Chunk {
|
||||
aa, bb, cc, dd := a, b, c, d
|
||||
|
||||
j := 0
|
||||
for i := 0; i < 16; i++ {
|
||||
X[i] = uint32(p[j]) | uint32(p[j+1])<<8 | uint32(p[j+2])<<16 | uint32(p[j+3])<<24
|
||||
j += 4
|
||||
}
|
||||
|
||||
// If this needs to be made faster in the future,
|
||||
// the usual trick is to unroll each of these
|
||||
// loops by a factor of 4; that lets you replace
|
||||
// the shift[] lookups with constants and,
|
||||
// with suitable variable renaming in each
|
||||
// unrolled body, delete the a, b, c, d = d, a, b, c
|
||||
// (or you can let the optimizer do the renaming).
|
||||
//
|
||||
// The index variables are uint so that % by a power
|
||||
// of two can be optimized easily by a compiler.
|
||||
|
||||
// Round 1.
|
||||
for i := uint(0); i < 16; i++ {
|
||||
x := i
|
||||
s := shift1[i%4]
|
||||
f := ((c ^ d) & b) ^ d
|
||||
a += f + X[x]
|
||||
a = bits.RotateLeft32(a, s)
|
||||
a, b, c, d = d, a, b, c
|
||||
}
|
||||
|
||||
// Round 2.
|
||||
for i := uint(0); i < 16; i++ {
|
||||
x := xIndex2[i]
|
||||
s := shift2[i%4]
|
||||
g := (b & c) | (b & d) | (c & d)
|
||||
a += g + X[x] + 0x5a827999
|
||||
a = bits.RotateLeft32(a, s)
|
||||
a, b, c, d = d, a, b, c
|
||||
}
|
||||
|
||||
// Round 3.
|
||||
for i := uint(0); i < 16; i++ {
|
||||
x := xIndex3[i]
|
||||
s := shift3[i%4]
|
||||
h := b ^ c ^ d
|
||||
a += h + X[x] + 0x6ed9eba1
|
||||
a = bits.RotateLeft32(a, s)
|
||||
a, b, c, d = d, a, b, c
|
||||
}
|
||||
|
||||
a += aa
|
||||
b += bb
|
||||
c += cc
|
||||
d += dd
|
||||
|
||||
p = p[_Chunk:]
|
||||
n += _Chunk
|
||||
}
|
||||
|
||||
dig.s[0] = a
|
||||
dig.s[1] = b
|
||||
dig.s[2] = c
|
||||
dig.s[3] = d
|
||||
return n
|
||||
}
|
123
password.go
123
password.go
|
@ -8,11 +8,17 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/wneessen/go-hibp/md4"
|
||||
)
|
||||
|
||||
// PwnedPassAPI is a HIBP Pwned Passwords API client
|
||||
type PwnedPassAPI struct {
|
||||
hibp *Client // References back to the parent HIBP client
|
||||
// References back to the parent HIBP client
|
||||
hibp *Client
|
||||
// Query parameter map for additional query parameters passed to request
|
||||
ParamMap map[string]string
|
||||
}
|
||||
|
||||
// Match represents a match in the Pwned Passwords API
|
||||
|
@ -21,17 +27,44 @@ type Match struct {
|
|||
Count int64 // Represents the number of leaked accounts that hold/held this password
|
||||
}
|
||||
|
||||
type HashMode int
|
||||
|
||||
const (
|
||||
// HashModeSHA1 is the default hash mode expecting SHA-1 hashes
|
||||
HashModeSHA1 HashMode = iota
|
||||
// HashModeNTLM represents the mode that expects and returns NTLM hashes
|
||||
HashModeNTLM
|
||||
)
|
||||
|
||||
// PwnedPasswordOptions is a struct of additional options for the PP API
|
||||
type PwnedPasswordOptions struct {
|
||||
// HashMode controls whether the provided hash is in SHA-1 or NTLM format
|
||||
// HashMode defaults to SHA-1 and can be overridden using the WithNTLMHash() Option
|
||||
// See: https://haveibeenpwned.com/API/v3#PwnedPasswordsNTLM
|
||||
HashMode HashMode
|
||||
|
||||
// WithPadding controls if the PwnedPassword API returns with padding or not
|
||||
// See: https://haveibeenpwned.com/API/v3#PwnedPasswordsPadding
|
||||
WithPadding bool
|
||||
}
|
||||
|
||||
// CheckPassword checks the Pwned Passwords database against a given password string
|
||||
//
|
||||
// This method will automatically decide whether the hash is in SHA-1 or NTLM format based on
|
||||
// the Option when the Client was initialized
|
||||
func (p *PwnedPassAPI) CheckPassword(pw string) (*Match, *http.Response, error) {
|
||||
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
||||
return p.CheckSHA1(shaSum)
|
||||
switch p.hibp.PwnedPassAPIOpts.HashMode {
|
||||
case HashModeSHA1:
|
||||
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
||||
return p.CheckSHA1(shaSum)
|
||||
case HashModeNTLM:
|
||||
d := md4.New()
|
||||
d.Write(stringToUTF16(pw))
|
||||
md4Sum := fmt.Sprintf("%x", d.Sum(nil))
|
||||
return p.CheckNTLM(md4Sum)
|
||||
default:
|
||||
return nil, nil, ErrUnsupportedHashMode
|
||||
}
|
||||
}
|
||||
|
||||
// CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password string
|
||||
|
@ -40,13 +73,34 @@ func (p *PwnedPassAPI) CheckSHA1(h string) (*Match, *http.Response, error) {
|
|||
return nil, nil, ErrSHA1LengthMismatch
|
||||
}
|
||||
|
||||
p.hibp.PwnedPassAPIOpts.HashMode = HashModeSHA1
|
||||
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
|
||||
if err != nil {
|
||||
return &Match{}, hr, err
|
||||
}
|
||||
|
||||
for _, m := range pwMatches {
|
||||
if m.Hash == h {
|
||||
if m.Hash == strings.ToLower(h) {
|
||||
return &m, hr, nil
|
||||
}
|
||||
}
|
||||
return nil, hr, nil
|
||||
}
|
||||
|
||||
// CheckNTLM checks the Pwned Passwords database against a given NTLM hash of a password string
|
||||
func (p *PwnedPassAPI) CheckNTLM(h string) (*Match, *http.Response, error) {
|
||||
if len(h) != 32 {
|
||||
return nil, nil, ErrNTLMLengthMismatch
|
||||
}
|
||||
|
||||
p.hibp.PwnedPassAPIOpts.HashMode = HashModeNTLM
|
||||
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
|
||||
if err != nil {
|
||||
return &Match{}, hr, err
|
||||
}
|
||||
|
||||
for _, m := range pwMatches {
|
||||
if m.Hash == strings.ToLower(h) {
|
||||
return &m, hr, nil
|
||||
}
|
||||
}
|
||||
|
@ -56,11 +110,24 @@ func (p *PwnedPassAPI) CheckSHA1(h string) (*Match, *http.Response, error) {
|
|||
// ListHashesPassword checks the Pwned Password API endpoint for all hashes based on a given
|
||||
// password string and returns the a slice of Match as well as the http.Response
|
||||
//
|
||||
// This method will automatically decide whether the hash is in SHA-1 or NTLM format based on
|
||||
// the Option when the Client was initialized
|
||||
//
|
||||
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
||||
// contain junk data
|
||||
func (p *PwnedPassAPI) ListHashesPassword(pw string) ([]Match, *http.Response, error) {
|
||||
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
||||
return p.ListHashesSHA1(shaSum)
|
||||
switch p.hibp.PwnedPassAPIOpts.HashMode {
|
||||
case HashModeSHA1:
|
||||
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
||||
return p.ListHashesSHA1(shaSum)
|
||||
case HashModeNTLM:
|
||||
d := md4.New()
|
||||
d.Write(stringToUTF16(pw))
|
||||
md4Sum := fmt.Sprintf("%x", d.Sum(nil))
|
||||
return p.ListHashesNTLM(md4Sum)
|
||||
default:
|
||||
return nil, nil, ErrUnsupportedHashMode
|
||||
}
|
||||
}
|
||||
|
||||
// ListHashesSHA1 checks the Pwned Password API endpoint for all hashes based on a given
|
||||
|
@ -72,6 +139,7 @@ func (p *PwnedPassAPI) ListHashesSHA1(h string) ([]Match, *http.Response, error)
|
|||
if len(h) != 40 {
|
||||
return nil, nil, ErrSHA1LengthMismatch
|
||||
}
|
||||
p.hibp.PwnedPassAPIOpts.HashMode = HashModeSHA1
|
||||
dst := make([]byte, hex.DecodedLen(len(h)))
|
||||
if _, err := hex.Decode(dst, []byte(h)); err != nil {
|
||||
return nil, nil, ErrSHA1Invalid
|
||||
|
@ -79,8 +147,28 @@ func (p *PwnedPassAPI) ListHashesSHA1(h string) ([]Match, *http.Response, error)
|
|||
return p.ListHashesPrefix(h[:5])
|
||||
}
|
||||
|
||||
// ListHashesNTLM checks the Pwned Password API endpoint for all hashes based on a given
|
||||
// NTLM hash and returns the a slice of Match as well as the http.Response
|
||||
//
|
||||
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
||||
// contain junk data
|
||||
func (p *PwnedPassAPI) ListHashesNTLM(h string) ([]Match, *http.Response, error) {
|
||||
if len(h) != 32 {
|
||||
return nil, nil, ErrNTLMLengthMismatch
|
||||
}
|
||||
p.hibp.PwnedPassAPIOpts.HashMode = HashModeNTLM
|
||||
dst := make([]byte, hex.DecodedLen(len(h)))
|
||||
if _, err := hex.Decode(dst, []byte(h)); err != nil {
|
||||
return nil, nil, ErrNTLMInvalid
|
||||
}
|
||||
return p.ListHashesPrefix(h[:5])
|
||||
}
|
||||
|
||||
// ListHashesPrefix checks the Pwned Password API endpoint for all hashes based on a given
|
||||
// SHA1 checksum prefix and returns the a slice of Match as well as the http.Response
|
||||
// SHA-1 or NTLM hash prefix and returns the a slice of Match as well as the http.Response
|
||||
//
|
||||
// To decide which HashType is queried for, make sure to set the appropriate HashMode in
|
||||
// the PwnedPassAPI struct
|
||||
//
|
||||
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
||||
// contain junk data
|
||||
|
@ -89,8 +177,16 @@ func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, err
|
|||
return nil, nil, ErrPrefixLengthMismatch
|
||||
}
|
||||
|
||||
switch p.hibp.PwnedPassAPIOpts.HashMode {
|
||||
case HashModeSHA1:
|
||||
delete(p.ParamMap, "mode")
|
||||
case HashModeNTLM:
|
||||
p.ParamMap["mode"] = "ntlm"
|
||||
default:
|
||||
delete(p.ParamMap, "mode")
|
||||
}
|
||||
au := fmt.Sprintf("%s/range/%s", PasswdBaseURL, pf)
|
||||
hreq, err := p.hibp.HTTPReq(http.MethodGet, au, nil)
|
||||
hreq, err := p.hibp.HTTPReq(http.MethodGet, au, p.ParamMap)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -129,3 +225,14 @@ func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, err
|
|||
|
||||
return pm, hr, nil
|
||||
}
|
||||
|
||||
// stringToUTF16 converts a given string to a UTF-16 little-endian encoded byte slice
|
||||
func stringToUTF16(s string) []byte {
|
||||
e := utf16.Encode([]rune(s))
|
||||
r := make([]byte, len(e)*2)
|
||||
for i := 0; i < len(e); i++ {
|
||||
r[i*2] = byte(e[i])
|
||||
r[i*2+1] = byte(e[i] << 8)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
162
password_test.go
162
password_test.go
|
@ -17,9 +17,17 @@ const (
|
|||
// Represents the string: test
|
||||
PwHashInsecure = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
|
||||
|
||||
// PwHashInsecure is the NTLM hash of an insecure password
|
||||
// Represents the string: test
|
||||
PwHashInsecureNTLM = "0cb6948805f797bf2a82807973b89537"
|
||||
|
||||
// PwHashSecure is the SHA1 checksum of a secure password
|
||||
// Represents the string: F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX
|
||||
PwHashSecure = "90efc095c82eab44e882fda507cfab1a2cd31fc0"
|
||||
|
||||
// PwHashSecureNTLM is the NTLM hash of a secure password
|
||||
// Represents the string: F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX
|
||||
PwHashSecureNTLM = "997f11041d9aa830842e682d1b4207df"
|
||||
)
|
||||
|
||||
// TestPwnedPassAPI_CheckPassword verifies the Pwned Passwords API with the CheckPassword method
|
||||
|
@ -29,7 +37,7 @@ func TestPwnedPassAPI_CheckPassword(t *testing.T) {
|
|||
pwString string
|
||||
isLeaked bool
|
||||
}{
|
||||
{"weak password 'test123' is expected to be leaked", PwStringInsecure, true},
|
||||
{"weak password 'test' is expected to be leaked", PwStringInsecure, true},
|
||||
{
|
||||
"strong, unknown password is expected to be not leaked",
|
||||
PwStringSecure, false,
|
||||
|
@ -53,6 +61,38 @@ func TestPwnedPassAPI_CheckPassword(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestPwnedPassAPI_CheckPassword_NTLM verifies the Pwned Passwords API with the CheckPassword method
|
||||
// with NTLM hashes enabled
|
||||
func TestPwnedPassAPI_CheckPassword_NTLM(t *testing.T) {
|
||||
testTable := []struct {
|
||||
testName string
|
||||
pwString string
|
||||
isLeaked bool
|
||||
}{
|
||||
{"weak password 'test' is expected to be leaked", PwStringInsecure, true},
|
||||
{
|
||||
"strong, unknown password is expected to be not leaked",
|
||||
PwStringSecure, false,
|
||||
},
|
||||
}
|
||||
hc := New(WithPwnedNTLMHash())
|
||||
for _, tc := range testTable {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
m, _, err := hc.PwnedPassAPI.CheckPassword(tc.pwString)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if m == nil && tc.isLeaked {
|
||||
t.Errorf("password is expected to be leaked but 0 leaks were returned in Pwned Passwords DB")
|
||||
}
|
||||
if m != nil && m.Count > 0 && !tc.isLeaked {
|
||||
t.Errorf("password is not expected to be leaked but %d leaks were found in Pwned Passwords DB",
|
||||
m.Count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPwnedPassAPI_CheckSHA1 verifies the Pwned Passwords API with the CheckSHA1 method
|
||||
func TestPwnedPassAPI_CheckSHA1(t *testing.T) {
|
||||
testTable := []struct {
|
||||
|
@ -89,6 +129,52 @@ func TestPwnedPassAPI_CheckSHA1(t *testing.T) {
|
|||
t.Errorf("password is not expected to be leaked but %d leaks were found in Pwned Passwords DB",
|
||||
m.Count)
|
||||
}
|
||||
if m != nil && m.Hash != tc.pwHash {
|
||||
t.Errorf("password hashes don't match, expected: %s, got %s", tc.pwHash, m.Hash)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPwnedPassAPI_CheckNTLM verifies the Pwned Passwords API with the CheckNTLM method
|
||||
func TestPwnedPassAPI_CheckNTLM(t *testing.T) {
|
||||
testTable := []struct {
|
||||
testName string
|
||||
pwHash string
|
||||
isLeaked bool
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
"weak password 'test' is expected to be leaked",
|
||||
PwHashInsecureNTLM, true, false,
|
||||
},
|
||||
{
|
||||
"strong, unknown password is expected to be not leaked",
|
||||
PwHashSecureNTLM, false, false,
|
||||
},
|
||||
{
|
||||
"empty string should fail",
|
||||
"", false, true,
|
||||
},
|
||||
}
|
||||
hc := New()
|
||||
for _, tc := range testTable {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
m, _, err := hc.PwnedPassAPI.CheckNTLM(tc.pwHash)
|
||||
if err != nil && !tc.shouldFail {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if m == nil && tc.isLeaked {
|
||||
t.Errorf("password is expected to be leaked but 0 leaks were returned in Pwned Passwords DB")
|
||||
}
|
||||
if m != nil && m.Count > 0 && !tc.isLeaked {
|
||||
t.Errorf("password is not expected to be leaked but %d leaks were found in Pwned Passwords DB",
|
||||
m.Count)
|
||||
}
|
||||
if m != nil && m.Hash != tc.pwHash {
|
||||
t.Errorf("password hashes don't match, expected: %s, got %s", tc.pwHash, m.Hash)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -188,6 +274,44 @@ func TestPwnedPassAPI_ListHashesSHA1_Errors(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestPwnedPassAPI_ListHashesNTLM_Errors tests the ListHashesNTLM method's errors
|
||||
func TestPwnedPassAPI_ListHashesNTLM_Errors(t *testing.T) {
|
||||
hc := New()
|
||||
|
||||
// Empty hash
|
||||
t.Run("empty hash", func(t *testing.T) {
|
||||
_, _, err := hc.PwnedPassAPI.ListHashesNTLM("")
|
||||
if err == nil {
|
||||
t.Errorf("ListHashesNTLM with empty hash should fail but didn't")
|
||||
}
|
||||
if !errors.Is(err, ErrNTLMLengthMismatch) {
|
||||
t.Errorf("ListHashesNTLM with empty hash should return ErrNTLMLengthMismatch error but didn't")
|
||||
}
|
||||
})
|
||||
|
||||
// Too long hash
|
||||
t.Run("too long hash", func(t *testing.T) {
|
||||
_, _, err := hc.PwnedPassAPI.ListHashesNTLM("FF36DC7D3284A39991ADA90CAF20D1E3C0DADEFAB")
|
||||
if err == nil {
|
||||
t.Errorf("ListHashesNTLM with too long hash should fail but didn't")
|
||||
}
|
||||
if !errors.Is(err, ErrNTLMLengthMismatch) {
|
||||
t.Errorf("ListHashesNTLM with too long hash should return ErrNTLMLengthMismatch error but didn't")
|
||||
}
|
||||
})
|
||||
|
||||
// Invalid hash
|
||||
t.Run("invalid hash", func(t *testing.T) {
|
||||
_, _, err := hc.PwnedPassAPI.ListHashesNTLM("3284A39991ADA90CAF20D1E3C0DADEFZ")
|
||||
if err == nil {
|
||||
t.Errorf("ListHashesNTLM with invalid hash should fail but didn't")
|
||||
}
|
||||
if !errors.Is(err, ErrNTLMInvalid) {
|
||||
t.Errorf("ListHashesNTLM with invalid hash should return ErrSHA1Invalid error but didn't")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPwnedPassApi_ListHashesSHA1 tests the PwnedPassAPI.ListHashesSHA1 metethod
|
||||
func TestPwnedPassAPI_ListHashesSHA1(t *testing.T) {
|
||||
hc := New()
|
||||
|
@ -208,6 +332,26 @@ func TestPwnedPassAPI_ListHashesSHA1(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestPwnedPassApi_ListHashesNTLM tests the PwnedPassAPI.ListHashesNTLM metethod
|
||||
func TestPwnedPassAPI_ListHashesNTLM(t *testing.T) {
|
||||
hc := New(WithPwnedNTLMHash())
|
||||
|
||||
// List length should be >0
|
||||
l, _, err := hc.PwnedPassAPI.ListHashesNTLM(PwHashInsecureNTLM)
|
||||
if err != nil {
|
||||
t.Errorf("ListHashesNTLM was not supposed to fail, but did: %s", err)
|
||||
}
|
||||
if len(l) <= 0 {
|
||||
t.Errorf("ListHashesNTLM was supposed to return a list longer than 0")
|
||||
}
|
||||
|
||||
// Hash has wrong size
|
||||
_, _, err = hc.PwnedPassAPI.ListHashesNTLM(PwStringInsecure)
|
||||
if err == nil {
|
||||
t.Errorf("ListHashesNTLM was supposed to fail, but didn't")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPwnedPassAPI_ListHashesPassword tests the PwnedPassAPI.ListHashesPassword metethod
|
||||
func TestPwnedPassAPI_ListHashesPassword(t *testing.T) {
|
||||
hc := New()
|
||||
|
@ -273,3 +417,19 @@ func ExamplePwnedPassAPI_checkSHA1() {
|
|||
// Output: Your password with the hash "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" was found 86495 times in the pwned passwords DB
|
||||
}
|
||||
}
|
||||
|
||||
// ExamplePwnedPassAPI_checkNTLM is a code example to show how to check a given password NTLM hash
|
||||
// against the HIBP passwords API using the CheckNTLM() method
|
||||
func ExamplePwnedPassAPI_checkNTLM() {
|
||||
hc := New()
|
||||
pwHash := "0cb6948805f797bf2a82807973b89537" // represents the PW: "test"
|
||||
m, _, err := hc.PwnedPassAPI.CheckNTLM(pwHash)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if m != nil && m.Count != 0 {
|
||||
fmt.Printf("Your password with the hash %q was found %d times in the pwned passwords DB\n",
|
||||
m.Hash, m.Count)
|
||||
// Output: Your password with the hash "0cb6948805f797bf2a82807973b89537" was found 86495 times in the pwned passwords DB
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue