add password utilities to crypto module

This commit is contained in:
Sky Johnson 2025-07-25 11:05:32 -05:00
parent 78b38ee544
commit 7397c3ebbc
3 changed files with 648 additions and 0 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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
-- ======================================================================