diff --git a/modules/crypto/crypto.go b/modules/crypto/crypto.go index 75189f4..dd217ac 100644 --- a/modules/crypto/crypto.go +++ b/modules/crypto/crypto.go @@ -9,10 +9,16 @@ import ( "crypto/sha512" "encoding/base64" "encoding/hex" + "fmt" "math/big" + "strings" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/google/uuid" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" ) func GetFunctionList() map[string]luajit.GoFunction { @@ -36,6 +42,16 @@ func GetFunctionList() map[string]luajit.GoFunction { "random_hex": random_hex, "random_string": random_string, "secure_compare": secure_compare, + "argon2_hash": argon2_hash, + "argon2_verify": argon2_verify, + "bcrypt_hash": bcrypt_hash, + "bcrypt_verify": bcrypt_verify, + "scrypt_hash": scrypt_hash, + "scrypt_verify": scrypt_verify, + "pbkdf2_hash": pbkdf2_hash, + "pbkdf2_verify": pbkdf2_verify, + "password_hash": password_hash, + "password_verify": password_verify, } } @@ -235,3 +251,298 @@ func secure_compare(s *luajit.State) int { s.PushBoolean(hmac.Equal([]byte(a), []byte(b))) return 1 } + +func argon2_hash(s *luajit.State) int { + password := s.ToString(1) + time := uint32(1) + memory := uint32(64 * 1024) + threads := uint8(4) + keyLen := uint32(32) + + if s.GetTop() >= 2 && !s.IsNil(2) { + time = uint32(s.ToNumber(2)) + } + if s.GetTop() >= 3 && !s.IsNil(3) { + memory = uint32(s.ToNumber(3)) + } + if s.GetTop() >= 4 && !s.IsNil(4) { + threads = uint8(s.ToNumber(4)) + } + if s.GetTop() >= 5 && !s.IsNil(5) { + keyLen = uint32(s.ToNumber(5)) + } + + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + s.PushNil() + s.PushString("failed to generate salt") + return 2 + } + + hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen) + encodedSalt := base64.RawStdEncoding.EncodeToString(salt) + encodedHash := base64.RawStdEncoding.EncodeToString(hash) + + result := fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", + memory, time, threads, encodedSalt, encodedHash) + + s.PushString(result) + return 1 +} + +func argon2_verify(s *luajit.State) int { + password := s.ToString(1) + hash := s.ToString(2) + + parts := strings.Split(hash, "$") + if len(parts) != 6 || parts[1] != "argon2id" { + s.PushBoolean(false) + return 1 + } + + var memory, time uint32 + var threads uint8 + if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil { + s.PushBoolean(false) + return 1 + } + + salt, err := base64.RawStdEncoding.DecodeString(parts[4]) + if err != nil { + s.PushBoolean(false) + return 1 + } + + expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) + if err != nil { + s.PushBoolean(false) + return 1 + } + + actualHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(expectedHash))) + s.PushBoolean(hmac.Equal(actualHash, expectedHash)) + return 1 +} + +func bcrypt_hash(s *luajit.State) int { + password := s.ToString(1) + cost := 12 + + if s.GetTop() >= 2 && !s.IsNil(2) { + cost = int(s.ToNumber(2)) + if cost < 4 || cost > 31 { + s.PushNil() + s.PushString("invalid cost (must be 4-31)") + return 2 + } + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), cost) + if err != nil { + s.PushNil() + s.PushString("bcrypt hash failed") + return 2 + } + + s.PushString(string(hash)) + return 1 +} + +func bcrypt_verify(s *luajit.State) int { + password := s.ToString(1) + hash := s.ToString(2) + + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + s.PushBoolean(err == nil) + return 1 +} + +func scrypt_hash(s *luajit.State) int { + password := s.ToString(1) + N := 32768 // CPU cost + r := 8 // block size + p := 1 // parallelization + keyLen := 32 // key length + + if s.GetTop() >= 2 && !s.IsNil(2) { + N = int(s.ToNumber(2)) + } + if s.GetTop() >= 3 && !s.IsNil(3) { + r = int(s.ToNumber(3)) + } + if s.GetTop() >= 4 && !s.IsNil(4) { + p = int(s.ToNumber(4)) + } + if s.GetTop() >= 5 && !s.IsNil(5) { + keyLen = int(s.ToNumber(5)) + } + + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + s.PushNil() + s.PushString("failed to generate salt") + return 2 + } + + hash, err := scrypt.Key([]byte(password), salt, N, r, p, keyLen) + if err != nil { + s.PushNil() + s.PushString("scrypt hash failed") + return 2 + } + + encodedSalt := base64.RawStdEncoding.EncodeToString(salt) + encodedHash := base64.RawStdEncoding.EncodeToString(hash) + + result := fmt.Sprintf("$scrypt$N=%d,r=%d,p=%d$%s$%s", N, r, p, encodedSalt, encodedHash) + s.PushString(result) + return 1 +} + +func scrypt_verify(s *luajit.State) int { + password := s.ToString(1) + hash := s.ToString(2) + + parts := strings.Split(hash, "$") + if len(parts) != 5 || parts[1] != "scrypt" { + s.PushBoolean(false) + return 1 + } + + var N, r, p int + if _, err := fmt.Sscanf(parts[2], "N=%d,r=%d,p=%d", &N, &r, &p); err != nil { + s.PushBoolean(false) + return 1 + } + + salt, err := base64.RawStdEncoding.DecodeString(parts[3]) + if err != nil { + s.PushBoolean(false) + return 1 + } + + expectedHash, err := base64.RawStdEncoding.DecodeString(parts[4]) + if err != nil { + s.PushBoolean(false) + return 1 + } + + actualHash, err := scrypt.Key([]byte(password), salt, N, r, p, len(expectedHash)) + if err != nil { + s.PushBoolean(false) + return 1 + } + + s.PushBoolean(hmac.Equal(actualHash, expectedHash)) + return 1 +} + +func pbkdf2_hash(s *luajit.State) int { + password := s.ToString(1) + iterations := 100000 + keyLen := 32 + + if s.GetTop() >= 2 && !s.IsNil(2) { + iterations = int(s.ToNumber(2)) + } + if s.GetTop() >= 3 && !s.IsNil(3) { + keyLen = int(s.ToNumber(3)) + } + + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + s.PushNil() + s.PushString("failed to generate salt") + return 2 + } + + hash := pbkdf2.Key([]byte(password), salt, iterations, keyLen, sha256.New) + encodedSalt := base64.RawStdEncoding.EncodeToString(salt) + encodedHash := base64.RawStdEncoding.EncodeToString(hash) + + result := fmt.Sprintf("$pbkdf2-sha256$i=%d$%s$%s", iterations, encodedSalt, encodedHash) + s.PushString(result) + return 1 +} + +func pbkdf2_verify(s *luajit.State) int { + password := s.ToString(1) + hash := s.ToString(2) + + parts := strings.Split(hash, "$") + if len(parts) != 5 || parts[1] != "pbkdf2-sha256" { + s.PushBoolean(false) + return 1 + } + + var iterations int + if _, err := fmt.Sscanf(parts[2], "i=%d", &iterations); err != nil { + s.PushBoolean(false) + return 1 + } + + salt, err := base64.RawStdEncoding.DecodeString(parts[3]) + if err != nil { + s.PushBoolean(false) + return 1 + } + + expectedHash, err := base64.RawStdEncoding.DecodeString(parts[4]) + if err != nil { + s.PushBoolean(false) + return 1 + } + + actualHash := pbkdf2.Key([]byte(password), salt, iterations, len(expectedHash), sha256.New) + s.PushBoolean(hmac.Equal(actualHash, expectedHash)) + return 1 +} + +func password_hash(s *luajit.State) int { + password := s.ToString(1) + algorithm := "argon2id" // default + + if s.GetTop() >= 2 && !s.IsNil(2) { + algorithm = s.ToString(2) + } + + switch algorithm { + case "argon2id": + s.PushString(password) + return argon2_hash(s) + case "bcrypt": + s.PushString(password) + if s.GetTop() >= 3 { + s.PushNumber(s.ToNumber(3)) + } + return bcrypt_hash(s) + case "scrypt": + s.PushString(password) + return scrypt_hash(s) + case "pbkdf2": + s.PushString(password) + return pbkdf2_hash(s) + default: + s.PushNil() + s.PushString("unsupported algorithm: " + algorithm) + return 2 + } +} + +func password_verify(s *luajit.State) int { + hash := s.ToString(2) + + // Auto-detect algorithm from hash format + if strings.HasPrefix(hash, "$argon2id$") { + return argon2_verify(s) + } else if strings.HasPrefix(hash, "$2a$") || strings.HasPrefix(hash, "$2b$") || strings.HasPrefix(hash, "$2y$") { + return bcrypt_verify(s) + } else if strings.HasPrefix(hash, "$scrypt$") { + return scrypt_verify(s) + } else if strings.HasPrefix(hash, "$pbkdf2-sha256$") { + return pbkdf2_verify(s) + } + + s.PushBoolean(false) + return 1 +} diff --git a/modules/crypto/crypto.lua b/modules/crypto/crypto.lua index 2d7e934..2bec604 100644 --- a/modules/crypto/crypto.lua +++ b/modules/crypto/crypto.lua @@ -365,4 +365,166 @@ function crypto.verify_integrity(check) return crypto.secure_compare(expected, check.hash) end +-- ====================================================================== +-- PASSWORD HASHING +-- ====================================================================== + +-- Generic password hashing (defaults to argon2id) +function crypto.hash_password(password, algorithm, options) + algorithm = algorithm or "argon2id" + options = options or {} + + local result, err + + if algorithm == "argon2id" then + local time = options.time or 1 + local memory = options.memory or 65536 -- 64MB in KB + local threads = options.threads or 4 + local keylen = options.keylen or 32 + result, err = moonshark.argon2_hash(password, time, memory, threads, keylen) + + elseif algorithm == "bcrypt" then + local cost = options.cost or 12 + result, err = moonshark.bcrypt_hash(password, cost) + + elseif algorithm == "scrypt" then + local N = options.N or 32768 + local r = options.r or 8 + local p = options.p or 1 + local keylen = options.keylen or 32 + result, err = moonshark.scrypt_hash(password, N, r, p, keylen) + + elseif algorithm == "pbkdf2" then + local iterations = options.iterations or 100000 + local keylen = options.keylen or 32 + result, err = moonshark.pbkdf2_hash(password, iterations, keylen) + + else + error("unsupported algorithm: " .. algorithm) + end + + if not result then + error(err) + end + return result +end + +-- Generic password verification (auto-detects algorithm) +function crypto.verify_password(password, hash) + return moonshark.password_verify(password, hash) +end + +-- ====================================================================== +-- ALGORITHM-SPECIFIC FUNCTIONS +-- ====================================================================== + +-- Argon2id hashing +function crypto.argon2_hash(password, options) + options = options or {} + local time = options.time or 1 + local memory = options.memory or 65536 + local threads = options.threads or 4 + local keylen = options.keylen or 32 + + local result, err = moonshark.argon2_hash(password, time, memory, threads, keylen) + if not result then error(err) end + return result +end + +function crypto.argon2_verify(password, hash) + return moonshark.argon2_verify(password, hash) +end + +-- bcrypt hashing +function crypto.bcrypt_hash(password, cost) + cost = cost or 12 + local result, err = moonshark.bcrypt_hash(password, cost) + if not result then error(err) end + return result +end + +function crypto.bcrypt_verify(password, hash) + return moonshark.bcrypt_verify(password, hash) +end + +-- scrypt hashing +function crypto.scrypt_hash(password, options) + options = options or {} + local N = options.N or 32768 + local r = options.r or 8 + local p = options.p or 1 + local keylen = options.keylen or 32 + + local result, err = moonshark.scrypt_hash(password, N, r, p, keylen) + if not result then error(err) end + return result +end + +function crypto.scrypt_verify(password, hash) + return moonshark.scrypt_verify(password, hash) +end + +-- PBKDF2 hashing +function crypto.pbkdf2_hash(password, iterations, keylen) + iterations = iterations or 100000 + keylen = keylen or 32 + + local result, err = moonshark.pbkdf2_hash(password, iterations, keylen) + if not result then error(err) end + return result +end + +function crypto.pbkdf2_verify(password, hash) + return moonshark.pbkdf2_verify(password, hash) +end + +-- ====================================================================== +-- PASSWORD CONFIG PRESETS +-- ====================================================================== + +function crypto.hash_password_fast(password, algorithm) + algorithm = algorithm or "argon2id" + + local options = { + argon2id = { time = 1, memory = 8192, threads = 1 }, + bcrypt = { cost = 10 }, + scrypt = { N = 16384, r = 8, p = 1 }, + pbkdf2 = { iterations = 50000 } + } + + return crypto.hash_password(password, algorithm, options[algorithm]) +end + +function crypto.hash_password_strong(password, algorithm) + algorithm = algorithm or "argon2id" + + local options = { + argon2id = { time = 3, memory = 131072, threads = 4 }, + bcrypt = { cost = 14 }, + scrypt = { N = 65536, r = 8, p = 2 }, + pbkdf2 = { iterations = 200000 } + } + + return crypto.hash_password(password, algorithm, options[algorithm]) +end + +-- ====================================================================== +-- UTILITY FUNCTIONS +-- ====================================================================== + +-- Detect algorithm from hash +function crypto.detect_algorithm(hash) + if hash:match("^%$argon2id%$") then + return "argon2id" + elseif hash:match("^%$2[aby]%$") then + return "bcrypt" + elseif hash:match("^%$scrypt%$") then + return "scrypt" + elseif hash:match("^%$pbkdf2%-sha256%$") then + return "pbkdf2" + else + return "unknown" + end +end + return crypto diff --git a/tests/crypto.lua b/tests/crypto.lua index ae11693..ba93654 100644 --- a/tests/crypto.lua +++ b/tests/crypto.lua @@ -301,6 +301,181 @@ test("Error Handling", function() assert_equal(crypto.is_uuid("12345"), false) end) +-- ====================================================================== +-- PASSWORD TESTS +-- ====================================================================== + +test("Password Hash and Verification", function() + local password = "hubba-ba-loo117!@#" + local hash = crypto.hash_password(password) + local hash_fast = crypto.hash_password_fast(password) + local hash_strong = crypto.hash_password_strong(password) + + assert(crypto.verify_password(password, hash)) + assert(crypto.verify_password(password, hash_fast)) + assert(crypto.verify_password(password, hash_strong)) + + assert(not crypto.verify_password("failure", hash)) + assert(not crypto.verify_password("failure", hash_fast)) + assert(not crypto.verify_password("failure", hash_strong)) +end) + +test("Algorithm-Specific Password Hashing", function() + local password = "test123!@#" + + -- Test each algorithm individually + local argon2_hash = crypto.hash_password(password, "argon2id") + local bcrypt_hash = crypto.hash_password(password, "bcrypt") + local scrypt_hash = crypto.hash_password(password, "scrypt") + local pbkdf2_hash = crypto.hash_password(password, "pbkdf2") + + assert(crypto.verify_password(password, argon2_hash)) + assert(crypto.verify_password(password, bcrypt_hash)) + assert(crypto.verify_password(password, scrypt_hash)) + assert(crypto.verify_password(password, pbkdf2_hash)) + + -- Verify wrong passwords fail + assert(not crypto.verify_password("wrong", argon2_hash)) + assert(not crypto.verify_password("wrong", bcrypt_hash)) + assert(not crypto.verify_password("wrong", scrypt_hash)) + assert(not crypto.verify_password("wrong", pbkdf2_hash)) +end) + +test("Algorithm Detection", function() + local password = "detectme123" + + local argon2_hash = crypto.hash_password(password, "argon2id") + local bcrypt_hash = crypto.hash_password(password, "bcrypt") + local scrypt_hash = crypto.hash_password(password, "scrypt") + local pbkdf2_hash = crypto.hash_password(password, "pbkdf2") + + assert(crypto.detect_algorithm(argon2_hash) == "argon2id") + assert(crypto.detect_algorithm(bcrypt_hash) == "bcrypt") + assert(crypto.detect_algorithm(scrypt_hash) == "scrypt") + assert(crypto.detect_algorithm(pbkdf2_hash) == "pbkdf2") + assert(crypto.detect_algorithm("invalid$format") == "unknown") +end) + +test("Custom Algorithm Options", function() + local password = "custom123" + + -- Test custom argon2id options + local custom_argon2 = crypto.hash_password(password, "argon2id", { + time = 2, + memory = 32768, + threads = 2 + }) + assert(crypto.verify_password(password, custom_argon2)) + + -- Test custom bcrypt cost + local custom_bcrypt = crypto.hash_password(password, "bcrypt", {cost = 10}) + assert(crypto.verify_password(password, custom_bcrypt)) + + -- Test custom scrypt parameters + local custom_scrypt = crypto.hash_password(password, "scrypt", { + N = 16384, + r = 4, + p = 2 + }) + assert(crypto.verify_password(password, custom_scrypt)) + + -- Test custom pbkdf2 iterations + local custom_pbkdf2 = crypto.hash_password(password, "pbkdf2", { + iterations = 50000 + }) + assert(crypto.verify_password(password, custom_pbkdf2)) +end) + +test("Direct Algorithm Functions", function() + local password = "direct123" + + -- Test direct algorithm calls + local argon2_direct = crypto.argon2_hash(password) + local bcrypt_direct = crypto.bcrypt_hash(password) + local scrypt_direct = crypto.scrypt_hash(password) + local pbkdf2_direct = crypto.pbkdf2_hash(password) + + assert(crypto.argon2_verify(password, argon2_direct)) + assert(crypto.bcrypt_verify(password, bcrypt_direct)) + assert(crypto.scrypt_verify(password, scrypt_direct)) + assert(crypto.pbkdf2_verify(password, pbkdf2_direct)) + + -- Test with custom options + local argon2_custom = crypto.argon2_hash(password, {time = 1, memory = 16384}) + local scrypt_custom = crypto.scrypt_hash(password, {N = 8192}) + + assert(crypto.argon2_verify(password, argon2_custom)) + assert(crypto.scrypt_verify(password, scrypt_custom)) +end) + +test("Security Level Presets", function() + local password = "preset123" + local algorithms = {"argon2id", "bcrypt", "scrypt", "pbkdf2"} + + for _, algo in ipairs(algorithms) do + local fast_hash = crypto.hash_password_fast(password, algo) + local strong_hash = crypto.hash_password_strong(password, algo) + + assert(crypto.verify_password(password, fast_hash)) + assert(crypto.verify_password(password, strong_hash)) + + -- Verify algorithm detection + assert(crypto.detect_algorithm(fast_hash) == algo) + assert(crypto.detect_algorithm(strong_hash) == algo) + end +end) + +test("Edge Cases and Error Handling", function() + -- Test empty password + local empty_hash = crypto.hash_password("") + assert(crypto.verify_password("", empty_hash)) + assert(not crypto.verify_password("not-empty", empty_hash)) + + -- Test long password + local long_password = string.rep("a", 1000) + local long_hash = crypto.hash_password(long_password) + assert(crypto.verify_password(long_password, long_hash)) + + -- Test unicode password + local unicode_password = "🔐password123🔑" + local unicode_hash = crypto.hash_password(unicode_password) + assert(crypto.verify_password(unicode_password, unicode_hash)) + + -- Test invalid hash formats + assert(not crypto.verify_password("test", "invalid-hash")) + assert(not crypto.verify_password("test", "$invalid$format$")) + + -- Test unsupported algorithm error + local success, err = pcall(crypto.hash_password, "test", "invalid-algo") + assert(not success) + assert(string.find(err, "unsupported algorithm")) +end) + +test("Cross-Algorithm Verification", function() + local password = "cross123" + + -- Create hashes with different algorithms + local hashes = { + crypto.hash_password(password, "argon2id"), + crypto.hash_password(password, "bcrypt"), + crypto.hash_password(password, "scrypt"), + crypto.hash_password(password, "pbkdf2") + } + + -- Each hash should only verify with correct password + for _, hash in ipairs(hashes) do + assert(crypto.verify_password(password, hash)) + assert(not crypto.verify_password("wrong", hash)) + end + + -- Hashes should be different from each other + for i = 1, #hashes do + for j = i + 1, #hashes do + assert(hashes[i] ~= hashes[j]) + end + end +end) + -- ====================================================================== -- PERFORMANCE TESTS -- ======================================================================