diff --git a/README.md b/README.md index 1883909..7854e70 100644 --- a/README.md +++ b/README.md @@ -467,22 +467,28 @@ choose, err := math.Binomial(20, 10) fib := math.Fibonacci(42) ``` -### GenerateSecret - Probably SecureTM +Ultimate Security Suite – When “Good Enough” Isn’t Good Enough ```go import "github.com/theHamdiz/it" -// When you need a secret that's totally random* -secret := it.GenerateSecret(32) -// * Usually uses crypto/rand, but if that fails... -// well, let's just say we get creative with time. -// It's like using your birthday as a password, -// but with nanoseconds. Security through obscurity! +// When you need a secret that's totally random*: +secret := it.GenerateSecret(32) +// *Usually uses crypto/rand, but if that fails... well, we get creative with time. +// It's like using your birthday as a password, but with nanoseconds. Security through obscurity! + +// When your password is too lazy to protect itself: +hashed, err := it.HashPassword("mySuperSecret", 12) +// Your password is sent to a rigorous bootcamp (bcrypt rounds), emerging as a hardened hash with its own unique salt. +// If the bootcamp fails, you'll get a polite error message. + +// Think your password can waltz past the velvet rope? +err = it.VerifyPassword(hashed, "mySuperSecret") +// If err is nil, congratulations—your password made the cut. +// Otherwise, it's like a bouncer telling you, "Not on the list, buddy." ``` -Perfect for when you need cryptographic strength secrets, unless you don't, in which case you'll get something that looks cryptographic enough to fool management. - -Now go forth and generate secrets that are definitely not predictable (most of the time). +Now go forth and generate cryptographically convincing secrets, hash those passwords like they’re training for a marathon, and verify them with the confidence of a seasoned doorman. Enjoy your Ultimate Security Suite—because sometimes, even security needs a little swagger. ### Config - Because Hardcoding is a Crime diff --git a/go.mod b/go.mod index de89a14..5f69750 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/crypto v0.32.0 // indirect golang.org/x/sys v0.29.0 // indirect ) diff --git a/go.sum b/go.sum index eaf4ec9..5e66016 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/it.go b/it.go index 6df0cad..032f160 100644 --- a/it.go +++ b/it.go @@ -51,6 +51,7 @@ import ( "github.com/theHamdiz/it/rl" "github.com/theHamdiz/it/sm" "github.com/theHamdiz/it/tk" + "golang.org/x/crypto/bcrypt" ) // =================================================== @@ -654,6 +655,25 @@ func GenerateSecret(numBytes int) string { return hex.EncodeToString(bytes) } +// HashPassword takes your oh-so-secret password and a cost factor (because apparently, more work equals more security), +// then returns a bcrypt hash or an error if things go sideways. Try not to be shocked by the complexity. +func HashPassword(password string, cost int) ([]byte, error) { + // Convert your string password to bytes because bcrypt is stuck in the dark ages of byte slices. + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), cost) + if err != nil { + // Oops! Something went wrong. Hopefully, you enjoy error messages as much as we do. + return nil, fmt.Errorf("failed to hash password (surprise!): %w", err) + } + return hashedPassword, nil +} + +// VerifyPassword compares a stored bcrypt hashed password with the plain text password you (hopefully) remembered. +// Returns nil if they match, otherwise an error. Yes, it's basically a one-way street: you can't decrypt, you can only compare. +func VerifyPassword(hashedPassword []byte, password string) error { + // Compare the hash with the password (converted to bytes, because we're stuck in byte land). + return bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)) +} + // ======================================================= // Configuration - Making Things Configurable Since 2025 // ======================================================= diff --git a/it_test.go b/it_test.go index f648b10..83e468d 100644 --- a/it_test.go +++ b/it_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/theHamdiz/it" + "golang.org/x/crypto/bcrypt" ) // TestRecoverPanicAndContinue tests panic recovery @@ -359,7 +360,7 @@ func TestWaitFor(t *testing.T) { func TestGenerateSecret(t *testing.T) { for secretLength := 4; secretLength <= 16; secretLength++ { seenSecrets := make(map[string]struct{}) - // Technically, duplicate secrets could be produced even + // Technically, duplicate secrets could be produced even // if working properly, but it's relatively unlikely. for i := 0; i < 10; i++ { secret := it.GenerateSecret(secretLength) @@ -375,6 +376,78 @@ func TestGenerateSecret(t *testing.T) { } } +// TestHashPassword ensures that HashPassword returns a valid bcrypt hash. +func TestHashPassword(t *testing.T) { + password := "mySecretPassword123" + cost := 12 + + hashed, err := it.HashPassword(password, cost) + if err != nil { + t.Fatalf("HashPassword returned an error (surprise!): %v", err) + } + if len(hashed) == 0 { + t.Fatal("Expected a non-empty hashed password; did you forget to hash it?") + } + + // Check that the resulting hash is valid by using bcrypt's CompareHashAndPassword. + if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil { + t.Errorf("bcrypt comparison failed: %v", err) + } +} + +// TestVerifyPassword_Correct verifies that VerifyPassword accepts the correct password. +func TestVerifyPassword_Correct(t *testing.T) { + password := "mySecretPassword123" + cost := 12 + + hashed, err := it.HashPassword(password, cost) + if err != nil { + t.Fatalf("HashPassword returned an error (not again!): %v", err) + } + + // The correct password should pass verification. + if err := it.VerifyPassword(hashed, password); err != nil { + t.Errorf("VerifyPassword failed for a correct password: %v", err) + } +} + +// TestVerifyPassword_Incorrect ensures that VerifyPassword rejects an incorrect password. +func TestVerifyPassword_Incorrect(t *testing.T) { + password := "mySecretPassword123" + wrongPassword := "wrongPassword" + cost := 12 + + hashed, err := it.HashPassword(password, cost) + if err != nil { + t.Fatalf("HashPassword returned an error (seriously?): %v", err) + } + + // The wrong password should not verify. + if err := it.VerifyPassword(hashed, wrongPassword); err == nil { + t.Error("VerifyPassword accepted an incorrect password (we thought you cared about security)") + } +} + +// TestHashProducesDifferentHashes verifies that the same password produces different hashes +// each time due to the random salt. Because if they're equal, then something is very wrong. +func TestHashProducesDifferentHashes(t *testing.T) { + password := "mySecretPassword123" + cost := 12 + + hashed1, err := it.HashPassword(password, cost) + if err != nil { + t.Fatalf("First HashPassword call failed: %v", err) + } + hashed2, err := it.HashPassword(password, cost) + if err != nil { + t.Fatalf("Second HashPassword call failed: %v", err) + } + + if string(hashed1) == string(hashed2) { + t.Error("Two hashes for the same password should not be equal (thanks, salt!)") + } +} + // TestStructuredLogging tests structured logging functionality func TestStructuredLogging(t *testing.T) { tmpFile, err := os.CreateTemp("", "structured_log_test")