diff --git a/src/prometheus.lua b/src/prometheus.lua index d5b27366..bb3eeeb4 100644 --- a/src/prometheus.lua +++ b/src/prometheus.lua @@ -20,7 +20,7 @@ if not pcall(function() end) then local oldMathRandom = math.random; math.random = function(a, b) - if not a and b then + if not a then return oldMathRandom(); end if not b then diff --git a/src/prometheus/pipeline.lua b/src/prometheus/pipeline.lua index fe21f853..bfc85e18 100644 --- a/src/prometheus/pipeline.lua +++ b/src/prometheus/pipeline.lua @@ -166,19 +166,37 @@ function Pipeline:apply(code, filename) else --> use secure random number generator local success, seed = pcall(function() - local seedStr = io.popen("openssl rand -hex 12"):read("*a"):gsub("\n", "").."" + if not io or not io.popen then + error("io.popen is unavailable") + end + local handle = io.popen("openssl rand -hex 12") + if not handle then + error("Failed to execute openssl") + end + local seedStr = handle:read("*a") + local ok = handle:close() + if ok == false or ok == nil then + error("openssl command failed") + end + if not seedStr then + error("Failed to read from openssl") + end + seedStr = seedStr:gsub("%s+", "") + if #seedStr ~= 24 or not seedStr:match("^%x+$") then + error("Invalid openssl output") + end + local seedNum = 0; - --> NOTE: tonumber caps at 1.844674407371e+19. So we use this instead. + --> NOTE: accumulate modulo 2147483647 so it fits in signed 32-bit integer without double overflow for i = 1, #seedStr do local char = seedStr:sub(i, i):lower() local digit = char:match("%d") and (char:byte() - 48) or (char:byte() - 87) - seedNum = seedNum * 16 + digit + seedNum = (seedNum * 16 + digit) % 2147483647 end - --> Random Number Generator in Lua 5.1 is limited to 9.007199254741e+15. - if _VERSION == "Lua 5.1" and not jit then - seedNum = seedNum % 9.007199254741e+15 + if seedNum == 0 then + seedNum = 1 end return seedNum @@ -188,7 +206,13 @@ function Pipeline:apply(code, filename) math.randomseed(seed) else logger:warn("OpenSSL is unavailable. Falling back to unix time."); - math.randomseed(os.time()) + local now = (os and os.time) and os.time() or 1 + local clock = (os and os.clock) and math.floor(os.clock() * 1000000) or 0 + local fallback = (now + clock) % 2147483647 + if fallback == 0 then + fallback = 1 + end + math.randomseed(fallback) end end diff --git a/tests/harness/random-seed.lua b/tests/harness/random-seed.lua new file mode 100644 index 00000000..fe83a647 --- /dev/null +++ b/tests/harness/random-seed.lua @@ -0,0 +1,85 @@ +-- @runtime lua51 luajit +require("src.prometheus") +local Prometheus = require("src.prometheus") +Prometheus.Logger.logLevel = Prometheus.Logger.LogLevel.Error + +local loadstring = loadstring or load + +-- 1. Verify math.random() polyfill behavior +for i = 1, 20 do + local val = math.random() + assert(type(val) == "number", "math.random() must return a number") + assert(val >= 0 and val < 1, "math.random() must be in [0, 1), got " .. tostring(val)) +end + +local intVal = math.random(10) +assert(intVal >= 1 and intVal <= 10 and intVal == math.floor(intVal), "math.random(10) out of range") + +local rangeVal = math.random(5, 15) +assert(rangeVal >= 5 and rangeVal <= 15 and rangeVal == math.floor(rangeVal), "math.random(5, 15) out of range") + +local largeVal = math.random(1, 2^40) +assert(largeVal >= 1 and largeVal <= 2^40, "math.random(1, 2^40) out of range") + +-- 2. Verify deterministic pipeline with explicit seed +local code = 'local a = 10; local b = 20; return a + b' +local configDeterministic = { + LuaVersion = "Lua51", + Seed = 12345, + NameGenerator = "Mangled", + Steps = { + { Name = "Vmify" }, + { Name = "EncryptStrings" }, + { Name = "ConstantArray" }, + } +} + +local pipe1 = Prometheus.Pipeline:fromConfig(configDeterministic) +local out1 = pipe1:apply(code, "test.lua") +local pipe2 = Prometheus.Pipeline:fromConfig(configDeterministic) +local out2 = pipe2:apply(code, "test.lua") + +assert(out1 == out2, "Configured seed must produce deterministic identical output") +local res1 = assert(loadstring(out1))() +assert(res1 == 30, "Obfuscated code with fixed seed must execute correctly, got " .. tostring(res1)) + +-- 3. Verify non-deterministic pipeline without configured seed (Seed = 0) +local configRandom = { + LuaVersion = "Lua51", + Seed = 0, + Steps = { + { Name = "Vmify" }, + { Name = "EncryptStrings" }, + { Name = "ConstantArray" }, + } +} + +local randomOutputs = {} +for i = 1, 5 do + local p = Prometheus.Pipeline:fromConfig(configRandom) + local o = p:apply(code, "test.lua") + local res = assert(loadstring(o))() + assert(res == 30, "Obfuscated code with random seed must execute correctly") + randomOutputs[i] = o +end + +local hasDifference = false +for i = 2, 5 do + if randomOutputs[i] ~= randomOutputs[1] then + hasDifference = true + break + end +end +assert(hasDifference, "Repeated runs without configured seed must produce different outputs") + +-- 4. Verify fallback path when OpenSSL / io.popen is unavailable +local savedPopen = io.popen +io.popen = nil +local pFallback = Prometheus.Pipeline:fromConfig(configRandom) +local oFallback = pFallback:apply(code, "fallback.lua") +io.popen = savedPopen + +local resFallback = assert(loadstring(oFallback))() +assert(resFallback == 30, "Fallback obfuscation must execute correctly") + +print("Random seed and math.random regression checks passed")