mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-11-22 21:00:51 +01:00
#27: Implement NTLM hash support for PwnedPassAPI
This PR implements support for NTLM hashes as announced by Troy Hunt: https://s.pebcak.de/@troyhunt@infosec.exchange/109833758367903768 For this we needed to be able to calculate MD4 hashes, as NTLM basically is calculated like this: `MD4(UTF-16LE(pw))`. For this we ported the official golang.org/x/crypto/md4 package, so we can still claim that "only depends on Go stdlib" A new Client option has been introduced: `WithPwnedNTLMHash`. If the client is initalized with this option, all generic methods (`ListHashesPassword` and `CheckPassword`) will operate on NTLM hashes. Additionally, there are now equivalent methods for checking passwords and listing hashes for NTLM: `CheckNTLM` and `ListHashesNTLM`
This commit is contained in:
parent
2b0b51ae17
commit
179cd36d7f
8 changed files with 636 additions and 12 deletions
32
hibp.go
32
hibp.go
|
@ -49,8 +49,18 @@ var (
|
||||||
// expected length
|
// expected length
|
||||||
ErrSHA1LengthMismatch = errors.New("SHA1 hash size needs to be 160 bits")
|
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 should be used if a given string does not represent a valid SHA1 hash
|
||||||
ErrSHA1Invalid = errors.New("not 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
|
// Client is the HIBP client object
|
||||||
|
@ -80,7 +90,10 @@ func New(options ...Option) Client {
|
||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
c.to = DefaultTimeout
|
c.to = DefaultTimeout
|
||||||
c.PwnedPassAPIOpts = &PwnedPasswordOptions{}
|
c.PwnedPassAPIOpts = &PwnedPasswordOptions{
|
||||||
|
HashMode: HashModeSHA1,
|
||||||
|
WithPadding: false,
|
||||||
|
}
|
||||||
c.ua = DefaultUserAgent
|
c.ua = DefaultUserAgent
|
||||||
|
|
||||||
// Set additional options
|
// Set additional options
|
||||||
|
@ -95,7 +108,10 @@ func New(options ...Option) Client {
|
||||||
c.hc = httpClient(c.to)
|
c.hc = httpClient(c.to)
|
||||||
|
|
||||||
// Associate the different HIBP service APIs with the Client
|
// 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.BreachAPI = &BreachAPI{hibp: &c}
|
||||||
c.PasteAPI = &PasteAPI{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
|
// HTTPReq performs an HTTP request to the corresponding API
|
||||||
func (c *Client) HTTPReq(m, p string, q map[string]string) (*http.Request, error) {
|
func (c *Client) HTTPReq(m, p string, q map[string]string) (*http.Request, error) {
|
||||||
u, err := url.Parse(p)
|
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) {
|
func TestNewWithPwnedPadding(t *testing.T) {
|
||||||
hc := New(WithPwnedPadding())
|
hc := New(WithPwnedPadding())
|
||||||
if !hc.PwnedPassAPIOpts.WithPadding {
|
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)
|
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
|
// TestNewWithApiKey tests the New() function with the API key set
|
||||||
func TestNewWithApiKey(t *testing.T) {
|
func TestNewWithApiKey(t *testing.T) {
|
||||||
apiKey := os.Getenv("HIBP_API_KEY")
|
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
|
||||||
|
}
|
115
password.go
115
password.go
|
@ -8,11 +8,17 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf16"
|
||||||
|
|
||||||
|
"github.com/wneessen/go-hibp/md4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PwnedPassAPI is a HIBP Pwned Passwords API client
|
// PwnedPassAPI is a HIBP Pwned Passwords API client
|
||||||
type PwnedPassAPI struct {
|
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
|
// 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
|
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
|
// PwnedPasswordOptions is a struct of additional options for the PP API
|
||||||
type PwnedPasswordOptions struct {
|
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
|
// WithPadding controls if the PwnedPassword API returns with padding or not
|
||||||
// See: https://haveibeenpwned.com/API/v3#PwnedPasswordsPadding
|
// See: https://haveibeenpwned.com/API/v3#PwnedPasswordsPadding
|
||||||
WithPadding bool
|
WithPadding bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPassword checks the Pwned Passwords database against a given password string
|
// 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) {
|
func (p *PwnedPassAPI) CheckPassword(pw string) (*Match, *http.Response, error) {
|
||||||
|
switch p.hibp.PwnedPassAPIOpts.HashMode {
|
||||||
|
case HashModeSHA1:
|
||||||
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
||||||
return p.CheckSHA1(shaSum)
|
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
|
// 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
|
return nil, nil, ErrSHA1LengthMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.hibp.PwnedPassAPIOpts.HashMode = HashModeSHA1
|
||||||
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
|
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &Match{}, hr, err
|
return &Match{}, hr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range pwMatches {
|
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
|
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
|
// 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
|
// 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
|
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
||||||
// contain junk data
|
// contain junk data
|
||||||
func (p *PwnedPassAPI) ListHashesPassword(pw string) ([]Match, *http.Response, error) {
|
func (p *PwnedPassAPI) ListHashesPassword(pw string) ([]Match, *http.Response, error) {
|
||||||
|
switch p.hibp.PwnedPassAPIOpts.HashMode {
|
||||||
|
case HashModeSHA1:
|
||||||
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
||||||
return p.ListHashesSHA1(shaSum)
|
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
|
// 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 {
|
if len(h) != 40 {
|
||||||
return nil, nil, ErrSHA1LengthMismatch
|
return nil, nil, ErrSHA1LengthMismatch
|
||||||
}
|
}
|
||||||
|
p.hibp.PwnedPassAPIOpts.HashMode = HashModeSHA1
|
||||||
dst := make([]byte, hex.DecodedLen(len(h)))
|
dst := make([]byte, hex.DecodedLen(len(h)))
|
||||||
if _, err := hex.Decode(dst, []byte(h)); err != nil {
|
if _, err := hex.Decode(dst, []byte(h)); err != nil {
|
||||||
return nil, nil, ErrSHA1Invalid
|
return nil, nil, ErrSHA1Invalid
|
||||||
|
@ -79,8 +147,28 @@ func (p *PwnedPassAPI) ListHashesSHA1(h string) ([]Match, *http.Response, error)
|
||||||
return p.ListHashesPrefix(h[:5])
|
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
|
// 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
|
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
||||||
// contain junk data
|
// contain junk data
|
||||||
|
@ -89,8 +177,16 @@ func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, err
|
||||||
return nil, nil, ErrPrefixLengthMismatch
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -129,3 +225,14 @@ func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, err
|
||||||
|
|
||||||
return pm, hr, nil
|
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
|
// Represents the string: test
|
||||||
PwHashInsecure = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
|
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
|
// PwHashSecure is the SHA1 checksum of a secure password
|
||||||
// Represents the string: F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX
|
// Represents the string: F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX
|
||||||
PwHashSecure = "90efc095c82eab44e882fda507cfab1a2cd31fc0"
|
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
|
// TestPwnedPassAPI_CheckPassword verifies the Pwned Passwords API with the CheckPassword method
|
||||||
|
@ -29,7 +37,7 @@ func TestPwnedPassAPI_CheckPassword(t *testing.T) {
|
||||||
pwString string
|
pwString string
|
||||||
isLeaked bool
|
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",
|
"strong, unknown password is expected to be not leaked",
|
||||||
PwStringSecure, false,
|
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
|
// TestPwnedPassAPI_CheckSHA1 verifies the Pwned Passwords API with the CheckSHA1 method
|
||||||
func TestPwnedPassAPI_CheckSHA1(t *testing.T) {
|
func TestPwnedPassAPI_CheckSHA1(t *testing.T) {
|
||||||
testTable := []struct {
|
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",
|
t.Errorf("password is not expected to be leaked but %d leaks were found in Pwned Passwords DB",
|
||||||
m.Count)
|
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
|
// TestPwnedPassApi_ListHashesSHA1 tests the PwnedPassAPI.ListHashesSHA1 metethod
|
||||||
func TestPwnedPassAPI_ListHashesSHA1(t *testing.T) {
|
func TestPwnedPassAPI_ListHashesSHA1(t *testing.T) {
|
||||||
hc := New()
|
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
|
// TestPwnedPassAPI_ListHashesPassword tests the PwnedPassAPI.ListHashesPassword metethod
|
||||||
func TestPwnedPassAPI_ListHashesPassword(t *testing.T) {
|
func TestPwnedPassAPI_ListHashesPassword(t *testing.T) {
|
||||||
hc := New()
|
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
|
// 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