From 38cd8d69200f6eee806bc46cfca8a8aaf09b5f09 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 12 Jan 2025 08:46:41 +0000 Subject: [PATCH] Add pin, hex --- go.mod | 2 + go.sum | 2 + hex/hex.go | 31 +++++++++ pin/pin.go | 37 +++++++++++ pin/pin_test.go | 173 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 245 insertions(+) create mode 100644 go.sum create mode 100644 hex/hex.go create mode 100644 pin/pin.go create mode 100644 pin/pin_test.go diff --git a/go.mod b/go.mod index 5081e27..43e268d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.grassecon.net/grassrootseconomics/common go 1.23.4 + +require golang.org/x/crypto v0.32.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f9c45b --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= diff --git a/hex/hex.go b/hex/hex.go new file mode 100644 index 0000000..a752e9e --- /dev/null +++ b/hex/hex.go @@ -0,0 +1,31 @@ +package utils + +import ( + "encoding/hex" + "strings" +) + +func NormalizeHex(s string) (string, error) { + if len(s) >= 2 { + if s[:2] == "0x" { + s = s[2:] + } + } + r, err := hex.DecodeString(s) + if err != nil { + return "", err + } + return hex.EncodeToString(r), nil +} + +func IsSameHex(left string, right string) bool { + bl, err := NormalizeHex(left) + if err != nil { + return false + } + br, err := NormalizeHex(left) + if err != nil { + return false + } + return strings.Compare(bl, br) == 0 +} diff --git a/pin/pin.go b/pin/pin.go new file mode 100644 index 0000000..004185b --- /dev/null +++ b/pin/pin.go @@ -0,0 +1,37 @@ +package utils + +import ( + "regexp" + + "golang.org/x/crypto/bcrypt" +) + +const ( + // Define the regex pattern as a constant + pinPattern = `^\d{4}$` + + //Allowed incorrect PIN attempts + AllowedPINAttempts = uint8(3) + +) + +// checks whether the given input is a 4 digit number +func IsValidPIN(pin string) bool { + match, _ := regexp.MatchString(pinPattern, pin) + return match +} + +// HashPIN uses bcrypt with 8 salt rounds to hash the PIN +func HashPIN(pin string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8) + if err != nil { + return "", err + } + return string(hash), nil +} + +// VerifyPIN compareS the hashed PIN with the plaintext PIN +func VerifyPIN(hashedPIN, pin string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin)) + return err == nil +} diff --git a/pin/pin_test.go b/pin/pin_test.go new file mode 100644 index 0000000..4937121 --- /dev/null +++ b/pin/pin_test.go @@ -0,0 +1,173 @@ +package utils + +import ( + "testing" + + "golang.org/x/crypto/bcrypt" +) + +func TestIsValidPIN(t *testing.T) { + tests := []struct { + name string + pin string + expected bool + }{ + { + name: "Valid PIN with 4 digits", + pin: "1234", + expected: true, + }, + { + name: "Valid PIN with leading zeros", + pin: "0001", + expected: true, + }, + { + name: "Invalid PIN with less than 4 digits", + pin: "123", + expected: false, + }, + { + name: "Invalid PIN with more than 4 digits", + pin: "12345", + expected: false, + }, + { + name: "Invalid PIN with letters", + pin: "abcd", + expected: false, + }, + { + name: "Invalid PIN with special characters", + pin: "12@#", + expected: false, + }, + { + name: "Empty PIN", + pin: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := IsValidPIN(tt.pin) + if actual != tt.expected { + t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected) + } + }) + } +} + +func TestHashPIN(t *testing.T) { + tests := []struct { + name string + pin string + }{ + { + name: "Valid PIN with 4 digits", + pin: "1234", + }, + { + name: "Valid PIN with leading zeros", + pin: "0001", + }, + { + name: "Empty PIN", + pin: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hashedPIN, err := HashPIN(tt.pin) + if err != nil { + t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err) + return + } + + if hashedPIN == "" { + t.Errorf("HashPIN(%q) returned an empty hash", tt.pin) + } + + // Ensure the hash can be verified with bcrypt + err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin)) + if tt.pin != "" && err != nil { + t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err) + } + }) + } +} + +func TestVerifyMigratedHashPin(t *testing.T) { + tests := []struct { + pin string + hash string + }{ + { + pin: "1234", + hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6", + }, + } + + for _, tt := range tests { + t.Run(tt.pin, func(t *testing.T) { + ok := VerifyPIN(tt.hash, tt.pin) + if !ok { + t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin) + } + }) + } +} + +func TestVerifyPIN(t *testing.T) { + tests := []struct { + name string + pin string + hashedPIN string + shouldPass bool + }{ + { + name: "Valid PIN verification", + pin: "1234", + hashedPIN: hashPINHelper("1234"), + shouldPass: true, + }, + { + name: "Invalid PIN verification with incorrect PIN", + pin: "5678", + hashedPIN: hashPINHelper("1234"), + shouldPass: false, + }, + { + name: "Invalid PIN verification with empty PIN", + pin: "", + hashedPIN: hashPINHelper("1234"), + shouldPass: false, + }, + { + name: "Invalid PIN verification with invalid hash", + pin: "1234", + hashedPIN: "invalidhash", + shouldPass: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := VerifyPIN(tt.hashedPIN, tt.pin) + if result != tt.shouldPass { + t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass) + } + }) + } +} + +// Helper function to hash a PIN for testing purposes +func hashPINHelper(pin string) string { + hashedPIN, err := HashPIN(pin) + if err != nil { + panic("Failed to hash PIN for test setup: " + err.Error()) + } + return hashedPIN +}