adds .local/share/millennium and easyeffects

This commit is contained in:
2026-08-06 18:23:36 +02:00
parent f92fb90275
commit 8dc466cbac
2581 changed files with 339035 additions and 0 deletions
@@ -0,0 +1,108 @@
-- Configuration and path helpers for extension installation
local millennium = require("millennium")
local fs = require("fs")
local json = require("json")
local utils = require("utils")
local logger = require("logger")
local M = {}
function M.is_windows()
return jit.os == "Windows"
end
function M.get_plugin_dir()
local backend_path = utils.get_backend_path()
if not backend_path then
return nil
end
return fs.parent_path(backend_path)
end
function M.get_extension_dir()
local plugin_dir = M.get_plugin_dir()
if not plugin_dir then
return nil
end
return fs.join(plugin_dir, "fake-header-extension")
end
function M.get_steam_config_dir()
local steam_path = millennium.steam_path()
logger:info("[install] Steam path: " .. tostring(steam_path))
local is_win = M.is_windows()
logger:info("[install] Platform: " .. (is_win and "Windows" or "Linux"))
if is_win then
-- Windows: %LOCALAPPDATA%\Steam\htmlcache\Default
local appdata = utils.getenv("LOCALAPPDATA")
if appdata then
return fs.join(appdata, "Steam", "htmlcache", "Default")
end
return nil
else
-- Linux: ~/.local/share/Steam/config/htmlcache/Default
-- Or symlink: ~/.steam/steam/config/htmlcache/Default
local home = utils.getenv("HOME")
if home then
return fs.join(home, ".steam", "steam", "config", "htmlcache", "Default")
end
return nil
end
end
-- Generic JSON file reader with logging
function M.read_json_file(path, label)
logger:info("[install] Reading " .. label .. ": " .. path)
if not fs.exists(path) then
logger:error("[install] " .. label .. " not found at: " .. path)
return nil
end
local content, err = utils.read_file(path)
if not content then
logger:error("[install] Failed to read " .. label .. ": " .. (err or "unknown error"))
return nil
end
local data, decode_err = json.decode(content)
if not data then
logger:error("[install] Failed to decode " .. label .. ": " .. (decode_err or "unknown error"))
return nil
end
return data
end
function M.read_extension_keys()
local extension_dir = M.get_extension_dir()
if not extension_dir then
logger:error("[install] Failed to get extension directory")
return nil
end
local keys = M.read_json_file(fs.join(extension_dir, "extension-keys.json"), "extension-keys.json")
if keys then
logger:info("[install] Extension ID: " .. tostring(keys.extensionId))
end
return keys
end
function M.read_manifest()
local extension_dir = M.get_extension_dir()
if not extension_dir then
logger:error("[install] Failed to get extension directory")
return nil
end
local manifest = M.read_json_file(fs.join(extension_dir, "manifest.json"), "manifest.json")
if manifest then
logger:info("[install] Extension: " .. tostring(manifest.name) .. " v" .. tostring(manifest.version))
end
return manifest
end
return M
@@ -0,0 +1,62 @@
-- Extension settings builder for Chrome preferences
local sys_utils = require("utils")
local install_utils = require("install_extension.utils")
local M = {}
-- Get Windows FILETIME (100-nanosecond intervals since January 1, 1601)
local function get_windows_file_time()
local epoch_diff = 11644473600 -- seconds between 1601 and 1970
local now = sys_utils.time() -- current Unix timestamp in seconds
local windows_time = (now + epoch_diff) * 10000000 -- convert to 100-ns intervals
return string.format("%.0f", windows_time)
end
function M.build_extension_settings(extension_dir, manifest)
local permissions = manifest.permissions or {}
local api_permissions = {}
for _, p in ipairs(permissions) do
-- Filter out URL patterns, keep API permissions
if not p:match("://") and not p:match("^<") and not p:match("^%*") then
table.insert(api_permissions, p)
end
end
local file_time = get_windows_file_time()
return {
active_permissions = {
api = #api_permissions > 0 and api_permissions or install_utils.empty_array(),
explicit_host = { "<all_urls>" },
manifest_permissions = install_utils.empty_array(),
scriptable_host = { "<all_urls>" },
},
commands = {},
content_settings = install_utils.empty_array(),
creation_flags = 38,
first_install_time = file_time,
from_webstore = false,
granted_permissions = {
api = #api_permissions > 0 and api_permissions or install_utils.empty_array(),
explicit_host = { "<all_urls>" },
manifest_permissions = install_utils.empty_array(),
scriptable_host = { "<all_urls>" },
},
incognito_content_settings = install_utils.empty_array(),
incognito_preferences = {},
last_update_time = file_time,
location = 4, -- kUnpacked (developer mode)
newAllowFileAccess = true,
path = extension_dir,
preferences = {},
regular_only_preferences = {},
state = 1, -- Enabled
was_installed_by_default = false,
was_installed_by_oem = false,
withholding_permissions = false,
}
end
return M
@@ -0,0 +1,67 @@
-- HMAC Context for computing MACs on Chrome preferences
local logger = require("logger")
local json_helpers = require("install_extension.json_helpers")
local sha2 = require("install_extension.sha2")
---@class HmacContext
---@field sid string
---@field seed_hex string
---@field seed_bytes string
local HmacContext = {}
HmacContext.__index = HmacContext
---@param sid string
---@param seed_hex string
---@return HmacContext
function HmacContext.new(sid, seed_hex)
local self = setmetatable({}, HmacContext)
self.sid = sid or ""
self.seed_hex = seed_hex or "" -- Empty for Steam CEF
self.seed_bytes = sha2.hex_to_bin(seed_hex or "")
return self
end
---@param json_path string
---@param value any
---@return string|nil
function HmacContext:compute_mac(json_path, value)
local cleaned = json_helpers.remove_empty_children(value)
-- Use sorted JSON for consistent key ordering (must match JS JSON.stringify)
local json_value = json_helpers.encode_sorted(cleaned)
json_value = json_helpers.escape_for_hmac(json_value)
local message = self.sid .. json_path .. json_value
logger:info("[install] Computing MAC for path: " .. json_path)
logger:info("[install] Message length: " .. #message)
logger:info("[install] JSON preview: " .. json_value:sub(1, 100) .. "...")
local mac = string.upper(sha2.hmac(sha2.sha256, self.seed_bytes, message))
if mac then
logger:info("[install] MAC: " .. mac:sub(1, 16) .. "...")
else
logger:error("[install] MAC computation returned nil!")
end
return mac
end
---@param macs table
---@return string|nil
function HmacContext:compute_super_mac(macs)
-- Use sorted JSON for consistent key ordering
local macs_json = json_helpers.encode_sorted(macs)
local message = self.sid .. macs_json
logger:info("[install] Computing super MAC...")
logger:info("[install] MACs JSON: " .. macs_json:sub(1, 100) .. "...")
local mac = string.upper(sha2.hmac(sha2.sha256, self.seed_bytes, message))
if mac then
logger:info("[install] Super MAC: " .. mac:sub(1, 16) .. "...")
else
logger:error("[install] Super MAC computation returned nil!")
end
return mac
end
return HmacContext
@@ -0,0 +1,299 @@
-- Steam Extension Installer for Millennium
-- Installs the fake-header-extension into Steam's Chromium preferences
local fs = require("fs")
local json = require("json")
local sys_utils = require("utils")
local logger = require("logger")
local config = require("install_extension.config")
local install_utils = require("install_extension.utils")
local extension_builder = require("install_extension.extension_builder")
local HmacContext = require("install_extension.hmac")
local windows = require("install_extension.windows")
local M = {}
-- Helper to load prefs file with fallback to empty table
local function load_prefs_file(path, label)
if not fs.exists(path) then
logger:warn("[install] " .. label .. " not found, will create new")
return {}
end
local data = config.read_json_file(path, label)
if data then
logger:info("[install] Loaded existing " .. label)
end
return data or {}
end
-- Set up extension in a prefs object
local function setup_extension_prefs(prefs, extension_id, extension_settings, hmac_ctx, ext_mac, dev_mode_mac, compute_super)
install_utils.ensure_nested(prefs, "extensions", "settings")
install_utils.ensure_nested(prefs, "extensions", "ui")
prefs.extensions.ui.developer_mode = true
prefs.extensions.settings[extension_id] = extension_settings
if hmac_ctx then
install_utils.ensure_nested(prefs, "protection", "macs", "extensions", "settings")
install_utils.ensure_nested(prefs, "protection", "ui")
if ext_mac then prefs.protection.macs.extensions.settings[extension_id] = ext_mac end
if dev_mode_mac then prefs.protection.ui.developer_mode = dev_mode_mac end
if compute_super then
local super_mac = hmac_ctx:compute_super_mac(prefs.protection.macs)
if super_mac then
prefs.protection.super_mac = super_mac
logger:info("[install] Super MAC computed")
end
end
end
end
local function kill_steam_webhelper()
if config.is_windows() then
local msg = "Steam has been closed to complete the Extendium plugin installation. Please restart Steam."
sys_utils.exec('start "" powershell -WindowStyle Hidden -Command "taskkill /F /IM steamwebhelper.exe; taskkill /F /IM steam.exe; Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show(\'' .. msg .. '\', \'Steam restart required - extendium\', [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning)"')
else
sys_utils.exec('sleep 1 && pkill -KILL -f steam')
local msg = "Steam has been closed to complete the Extendium plugin installation.\\nPlease restart Steam."
sys_utils.exec('zenity --info --title "Steam restart required - extendium" --text "' .. msg .. '" &')
end
logger:info("[install] Steam webhelper killed")
end
function M.install()
logger:info("========================================")
logger:info("[install] Starting extension installation...")
logger:info("========================================")
-- Get paths
local extension_dir = config.get_extension_dir()
if not extension_dir then
logger:error("[install] Failed to get extension directory")
return json.encode({ success = false, error = "Failed to get extension directory" })
end
logger:info("[install] Extension directory: " .. extension_dir)
if not fs.exists(extension_dir) then
logger:error("[install] Extension directory does not exist: " .. extension_dir)
return json.encode({ success = false, error = "Extension directory not found" })
end
-- Read extension keys (contains pre-computed extension ID)
local keys = config.read_extension_keys()
if not keys or not keys.extensionId then
return json.encode({ success = false, error = "Failed to read extension keys" })
end
local extension_id = keys.extensionId
-- Read manifest
local manifest = config.read_manifest()
if not manifest then
return json.encode({ success = false, error = "Failed to read manifest" })
end
-- Get Steam config directory
local config_dir = config.get_steam_config_dir()
if not config_dir then
logger:error("[install] Failed to determine Steam config directory")
return json.encode({ success = false, error = "Failed to determine Steam config directory" })
end
logger:info("[install] Steam config directory: " .. config_dir)
if not fs.exists(config_dir) then
logger:error("[install] Steam config directory does not exist: " .. config_dir)
return json.encode({ success = false, error = "Steam config directory not found. Has Steam been run?" })
end
-- Resolve extension path to canonical form
local canonical_extension_dir = fs.canonical(extension_dir)
if canonical_extension_dir then
extension_dir = canonical_extension_dir
logger:info("[install] Canonical extension path: " .. extension_dir)
end
-- Build extension settings
local extension_settings = extension_builder.build_extension_settings(extension_dir, manifest)
logger:info("[install] Built extension settings")
-- Initialize HMAC context (Windows only)
---@type HmacContext|nil
local hmac_ctx = nil
if config.is_windows() then
logger:info("[install] Initializing HMAC context for Windows...")
local sid = windows.get_windows_sid()
if sid and sid ~= "" then
-- Steam CEF uses empty seed (like Edge/Brave)
hmac_ctx = HmacContext.new(sid, "")
logger:info("[install] HMAC context initialized")
else
logger:warn("[install] Could not get Windows SID, HMAC will be skipped")
end
else
logger:info("[install] Linux detected, HMAC not required")
end
-- ========================================================================
-- PHASE 1: Read all files and compute everything BEFORE writing
-- ========================================================================
logger:info("[install] Phase 1: Reading files and computing signatures...")
local prefs_path = fs.join(config_dir, "Preferences")
local secure_prefs_path = fs.join(config_dir, "Secure Preferences")
local prefs = load_prefs_file(prefs_path, "Preferences")
local secure_prefs = fs.exists(secure_prefs_path) and load_prefs_file(secure_prefs_path, "Secure Preferences") or nil
-- Compute HMACs if needed (Windows only) - do this BEFORE modifying prefs
local ext_mac, dev_mode_mac
if hmac_ctx then
logger:info("[install] Computing HMAC signatures...")
ext_mac = hmac_ctx:compute_mac("extensions.settings." .. extension_id, extension_settings)
if ext_mac then logger:info("[install] Extension MAC computed") end
dev_mode_mac = hmac_ctx:compute_mac("extensions.ui.developer_mode", true)
if dev_mode_mac then logger:info("[install] Developer mode MAC computed") end
end
-- Set up both Preferences and Secure Preferences
setup_extension_prefs(prefs, extension_id, extension_settings, hmac_ctx, ext_mac, dev_mode_mac, false)
if secure_prefs then
setup_extension_prefs(secure_prefs, extension_id, extension_settings, hmac_ctx, ext_mac, dev_mode_mac, true)
end
-- Pre-encode JSON to minimize time between writes and kill
logger:info("[install] Encoding JSON...")
local prefs_json = json.encode(prefs)
local secure_prefs_json = secure_prefs and json.encode(secure_prefs) or nil
-- ========================================================================
-- PHASE 2: Write files and kill Steam IMMEDIATELY after
-- ========================================================================
logger:info("[install] Phase 2: Writing files and killing Steam...")
-- Write Preferences
local success, write_err = sys_utils.write_file(prefs_path, prefs_json)
if not success then
logger:error("[install] Failed to write Preferences: " .. (write_err or "unknown"))
return json.encode({ success = false, error = "Failed to write Preferences" })
end
logger:info("[install] Preferences written")
-- Write Secure Preferences
if secure_prefs_json then
success, write_err = sys_utils.write_file(secure_prefs_path, secure_prefs_json)
if not success then
logger:error("[install] Failed to write Secure Preferences: " .. (write_err or "unknown"))
return json.encode({ success = false, error = "Failed to write Secure Preferences" })
end
logger:info("[install] Secure Preferences written")
end
logger:info("========================================")
logger:info("[install] Installation complete! Please restart Steam!")
logger:info("[install] Extension ID: " .. extension_id)
logger:info("[install] Extension Path: " .. extension_dir)
logger:info("========================================")
-- Kill Steam webhelper IMMEDIATELY after writing to prevent it from overriding our changes
kill_steam_webhelper()
return json.encode({
success = true,
extensionId = extension_id,
extensionPath = extension_dir,
message = "Extension installed. Restart Steam to load it."
})
end
local function check_extension_in_prefs(prefs, extension_id)
return prefs.extensions ~= nil and
prefs.extensions.settings ~= nil and
prefs.extensions.settings[extension_id] ~= nil
end
function M.check_status()
logger:info("[check_status] Checking installation status...")
local keys = config.read_extension_keys()
if not keys or not keys.extensionId then
return json.encode({ installed = false, error = "Extension keys not found" })
end
local config_dir = config.get_steam_config_dir()
if not config_dir then
return json.encode({ installed = false, error = "Steam config directory not found" })
end
local installed = false
local needs_cleanup = false
local prefs_path = fs.join(config_dir, "Preferences")
local secure_prefs_path = fs.join(config_dir, "Secure Preferences")
local prefs = load_prefs_file(prefs_path, "Preferences")
local secure_prefs = fs.exists(secure_prefs_path) and load_prefs_file(secure_prefs_path, "Secure Preferences") or nil
if prefs and check_extension_in_prefs(prefs, keys.extensionId) then
logger:info("[check_status] Extension found in Preferences")
local ext_path = prefs.extensions.settings[keys.extensionId].path
if ext_path and not fs.exists(ext_path) then
logger:warn("[check_status] Extension path does not exist: " .. ext_path)
needs_cleanup = true
else
installed = true
end
end
if secure_prefs and check_extension_in_prefs(secure_prefs, keys.extensionId) then
logger:info("[check_status] Extension found in Secure Preferences")
local ext_path = secure_prefs.extensions.settings[keys.extensionId].path
if ext_path and not fs.exists(ext_path) then
logger:warn("[check_status] Extension path does not exist in Secure Preferences: " .. ext_path)
needs_cleanup = true
else
installed = true
end
end
if needs_cleanup then
logger:info("[check_status] Cleaning up invalid extension entries...")
if prefs and check_extension_in_prefs(prefs, keys.extensionId) then
prefs.extensions.settings[keys.extensionId] = nil
local prefs_json = json.encode(prefs)
local success, write_err = sys_utils.write_file(prefs_path, prefs_json)
if success then
logger:info("[check_status] Removed invalid extension from Preferences")
else
logger:error("[check_status] Failed to clean Preferences: " .. (write_err or "unknown"))
end
end
if secure_prefs and check_extension_in_prefs(secure_prefs, keys.extensionId) then
secure_prefs.extensions.settings[keys.extensionId] = nil
local secure_prefs_json = json.encode(secure_prefs)
local success, write_err = sys_utils.write_file(secure_prefs_path, secure_prefs_json)
if success then
logger:info("[check_status] Removed invalid extension from Secure Preferences")
else
logger:error("[check_status] Failed to clean Secure Preferences: " .. (write_err or "unknown"))
end
end
kill_steam_webhelper()
installed = false
end
logger:info("[check_status] Installation status: " .. (installed and "installed" or "not installed"))
return json.encode({
installed = installed,
extensionId = keys.extensionId
})
end
return M
@@ -0,0 +1,108 @@
-- JSON helpers for HMAC computation
local json = require("json")
local M = {}
-- Encode JSON with sorted keys (critical for HMAC - must match JS JSON.stringify order)
function M.encode_sorted(value)
local t = type(value)
if value == nil then
return "null"
elseif t == "boolean" then
return value and "true" or "false"
elseif t == "number" then
if value == math.floor(value) and value >= -2147483648 and value <= 2147483647 then
return string.format("%d", value)
else
return string.format("%.14g", value)
end
elseif t == "string" then
-- Use cjson for proper string escaping
return json.encode(value)
elseif t == "table" then
-- Check if array (sequential integer keys starting at 1)
local is_array = false
local max_idx = 0
local count = 0
for k, _ in pairs(value) do
count = count + 1
if type(k) == "number" and k == math.floor(k) and k >= 1 then
if k > max_idx then max_idx = k end
end
end
-- It's an array if all keys are sequential integers 1..n
if count > 0 and max_idx == count then
is_array = true
for i = 1, max_idx do
if value[i] == nil then
is_array = false
break
end
end
end
-- Empty table - check metatable hint
if next(value) == nil then
local mt = getmetatable(value)
if mt and mt.__jsontype == "array" then
return "[]"
end
-- Default empty table to object
return "{}"
end
if is_array then
local parts = {}
for i = 1, max_idx do
parts[i] = M.encode_sorted(value[i])
end
return "[" .. table.concat(parts, ",") .. "]"
else
-- Object - sort keys alphabetically (matches JS insertion order in our case)
local keys = {}
for k, v in pairs(value) do
-- Only include string keys with non-nil values
if type(k) == "string" and v ~= nil then
table.insert(keys, k)
end
end
table.sort(keys)
local parts = {}
for _, k in ipairs(keys) do
local v = value[k]
-- Double-check for nil (shouldn't happen but safety net)
if v ~= nil then
table.insert(parts, json.encode(k) .. ":" .. M.encode_sorted(v))
end
end
return "{" .. table.concat(parts, ",") .. "}"
end
else
return "null"
end
end
-- Remove empty tables and arrays (Chromium's DeepCopyWithoutEmptyChildren)
function M.remove_empty_children(obj)
if type(obj) ~= "table" then return obj end
if next(obj) == nil then return nil end
local cleaned = {}
for k, v in pairs(obj) do
local cleaned_v = M.remove_empty_children(v)
if cleaned_v ~= nil and (type(cleaned_v) ~= "table" or next(cleaned_v) ~= nil) then
cleaned[k] = cleaned_v
end
end
return next(cleaned) ~= nil and cleaned or nil
end
-- Escape '<' in JSON string (Chromium requirement)
function M.escape_for_hmac(json_str)
return json_str:gsub("<", "\\u003C")
end
return M
@@ -0,0 +1,276 @@
--------------------------------------------------------------------------------------------------------------------------
-- sha2.lua (Minimal version for HMAC-SHA256)
-- AUTHOR: Egor Skriptunoff
-- LICENSE: MIT (the same license as Lua itself)
-- URL: https://github.com/Egor-Skriptunoff/pure_lua_SHA
-- Only contains sha256 and hmac functions for use with hmac.lua
-- Optimized for LuaJIT 5.1
--------------------------------------------------------------------------------------------------------------------------
local unpack, table_concat, byte, char, string_rep, sub, gsub, string_format, floor =
table.unpack or unpack, table.concat, string.byte, string.char, string.rep, string.sub, string.gsub, string.format, math.floor
--------------------------------------------------------------------------------
-- BRANCH DETECTION (LuaJIT)
--------------------------------------------------------------------------------
local is_LuaJIT = jit and jit.version
local branch = is_LuaJIT and "LJ" or "EMUL"
--------------------------------------------------------------------------------
-- BITWISE OPERATORS (LJ branch)
--------------------------------------------------------------------------------
local AND, OR, XOR, SHL, SHR, ROL, ROR, NOT, NORM, HEX, XOR_BYTE
if branch == "LJ" then
local b = bit or bit32
local library_name = b and (b.bit32 and "bit32" or "bit")
AND = b.band
OR = b.bor
XOR = b.bxor
SHL = b.lshift
SHR = b.rshift
ROL = b.rol or b.lrotate
ROR = b.ror or b.rrotate
NOT = b.bnot
NORM = b.tobit
HEX = b.tohex
XOR_BYTE = XOR
else
error("This minimal sha2.lua only supports LuaJIT with bit library")
end
--------------------------------------------------------------------------------
-- DATA TABLES
--------------------------------------------------------------------------------
local sha2_K_hi, sha2_H_hi = {}, {}
local sha2_H_ext256 = {[224] = {}, [256] = sha2_H_hi}
local common_W = {}
local K_lo_modulo = 4294967296
--------------------------------------------------------------------------------
-- MAGIC NUMBERS CALCULATOR (for SHA-256 constants)
--------------------------------------------------------------------------------
do
local function mul(src1, src2, factor, result_length)
local result, carry, value, weight = {}, 0.0, 0.0, 1.0
for j = 1, result_length do
for k = math.max(1, j + 1 - #src2), math.min(j, #src1) do
carry = carry + factor * src1[k] * src2[j + 1 - k]
end
local digit = carry % 2^24
result[j] = floor(digit)
carry = (carry - digit) / 2^24
value = value + digit * weight
weight = weight * 2^24
end
return result, value
end
local idx, step, p, one, sqrt_hi = 0, {4, 1, 2, -2, 2}, 4, {1}, sha2_H_hi
repeat
p = p + step[p % 6]
local d = 1
repeat
d = d + step[d % 6]
if d*d > p then
local root = p^(1/3)
local R = root * 2^40
R = mul({R - R % 1}, one, 1.0, 2)
local _, delta = mul(R, mul(R, R, 1.0, 4), -1.0, 4)
local hi = R[2] % 65536 * 65536 + floor(R[1] / 256)
local lo = R[1] % 256 * 16777216 + floor(delta * (2^-56 / 3) * root / p)
if idx < 8 then
root = p^(1/2)
R = root * 2^40
R = mul({R - R % 1}, one, 1.0, 2)
_, delta = mul(R, R, -1.0, 2)
local hi = R[2] % 65536 * 65536 + floor(R[1] / 256)
local lo = R[1] % 256 * 16777216 + floor(delta * 2^-17 / root)
local idx = idx % 8 + 1
sha2_H_ext256[224][idx] = lo
sqrt_hi[idx] = hi
end
idx = idx + 1
sha2_K_hi[idx] = hi
break
end
until p % d == 0
until idx > 63
end
--------------------------------------------------------------------------------
-- SHA256 FEED FUNCTION (LJ branch)
--------------------------------------------------------------------------------
function sha256_feed_64(H, str, offs, size)
local W, K = common_W, sha2_K_hi
for pos = offs, offs + size - 1, 64 do
for j = 1, 16 do
pos = pos + 4
local a, b, c, d = byte(str, pos - 3, pos)
W[j] = OR(SHL(a, 24), SHL(b, 16), SHL(c, 8), d)
end
for j = 17, 64 do
local a, b = W[j-15], W[j-2]
W[j] = NORM( NORM( XOR(ROR(a, 7), ROL(a, 14), SHR(a, 3)) + XOR(ROL(b, 15), ROL(b, 13), SHR(b, 10)) ) + NORM( W[j-7] + W[j-16] ) )
end
local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8]
for j = 1, 64, 8 do
local z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j] + W[j] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+1] + W[j+1] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+2] + W[j+2] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+3] + W[j+3] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+4] + W[j+4] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+5] + W[j+5] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+6] + W[j+6] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
z = NORM( XOR(ROR(e, 6), ROR(e, 11), ROL(e, 7)) + XOR(g, AND(e, XOR(f, g))) + (K[j+7] + W[j+7] + h) )
h, g, f, e = g, f, e, NORM(d + z)
d, c, b, a = c, b, a, NORM( XOR(AND(a, XOR(b, c)), AND(b, c)) + XOR(ROR(a, 2), ROR(a, 13), ROL(a, 10)) + z )
end
H[1], H[2], H[3], H[4] = NORM(a + H[1]), NORM(b + H[2]), NORM(c + H[3]), NORM(d + H[4])
H[5], H[6], H[7], H[8] = NORM(e + H[5]), NORM(f + H[6]), NORM(g + H[7]), NORM(h + H[8])
end
end
--------------------------------------------------------------------------------
-- MAIN FUNCTIONS
--------------------------------------------------------------------------------
local function sha256ext(width, message)
local H, length, tail = {unpack(sha2_H_ext256[width])}, 0.0, ""
local function partial(message_part)
if message_part then
if tail then
length = length + #message_part
local offs = 0
if tail ~= "" and #tail + #message_part >= 64 then
offs = 64 - #tail
sha256_feed_64(H, tail..sub(message_part, 1, offs), 0, 64)
tail = ""
end
local size = #message_part - offs
local size_tail = size % 64
sha256_feed_64(H, message_part, offs, size - size_tail)
tail = tail..sub(message_part, #message_part + 1 - size_tail)
return partial
else
error("Adding more chunks is not allowed after receiving the result", 2)
end
else
if tail then
local final_blocks = {tail, "\128", string_rep("\0", (-9 - length) % 64 + 1)}
tail = nil
length = length * (8 / 256^7)
for j = 4, 10 do
length = length % 1 * 256
final_blocks[j] = char(floor(length))
end
final_blocks = table_concat(final_blocks)
sha256_feed_64(H, final_blocks, 0, #final_blocks)
local max_reg = width / 32
for j = 1, max_reg do
H[j] = HEX(H[j])
end
H = table_concat(H, "", 1, max_reg)
end
return H
end
end
if message then
return partial(message)()
else
return partial
end
end
--------------------------------------------------------------------------------
-- HELPER FUNCTIONS
--------------------------------------------------------------------------------
local hex_to_bin
do
function hex_to_bin(hex_string)
return (gsub(hex_string, "%x%x",
function (hh)
return char(tonumber(hh, 16))
end
))
end
end
local function pad_and_xor(str, result_length, byte_for_xor)
return gsub(str, ".",
function(c)
return char(XOR_BYTE(byte(c), byte_for_xor))
end
)..string_rep(char(byte_for_xor), result_length - #str)
end
local block_size_for_HMAC
---@return string
local function hmac(hash_func, key, message)
local block_size = block_size_for_HMAC[hash_func]
if not block_size then
error("Unknown hash function", 2)
end
if #key > block_size then
key = hex_to_bin(hash_func(key))
end
local append = hash_func()(pad_and_xor(key, block_size, 0x36))
local result
local function partial(message_part)
if not message_part then
result = result or hash_func(pad_and_xor(key, block_size, 0x5C)..hex_to_bin(append()))
return result
elseif result then
error("Adding more chunks is not allowed after receiving the result", 2)
else
append(message_part)
return partial
end
end
if message then
return partial(message)()
else
return partial
end
end
--------------------------------------------------------------------------------
-- MODULE EXPORT
--------------------------------------------------------------------------------
local sha = {
sha256 = function (message) return sha256ext(256, message) end,
hmac = hmac,
hex_to_bin = hex_to_bin,
}
block_size_for_HMAC = {
[sha.sha256] = 64,
}
return sha
@@ -0,0 +1,24 @@
-- Utility helpers for extension installation
local M = {}
-- Helper to create empty arrays that serialize as [] not {}
-- cjson treats tables with no elements ambiguously, this ensures array serialization
function M.empty_array()
local arr = {}
setmetatable(arr, { __jsontype = "array" })
return arr
end
-- Ensure nested table path exists, creating tables as needed
-- Usage: ensure_nested(t, "a", "b", "c") ensures t.a.b.c exists
function M.ensure_nested(t, ...)
local current = t
for _, key in ipairs({...}) do
if not current[key] then current[key] = {} end
current = current[key]
end
return current
end
return M
@@ -0,0 +1,42 @@
-- Windows-specific utilities
local sys_utils = require("utils")
local logger = require("logger")
local config = require("install_extension.config")
local M = {}
function M.get_windows_sid()
if not config.is_windows() then
return ""
end
logger:info("[install] Getting Windows SID...")
local cmd = 'powershell -Command "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value"'
local output, status = sys_utils.exec(cmd)
if not output then
logger:error("[install] Failed to get Windows SID: " .. tostring(status))
return ""
end
local sid = sys_utils.trim(output)
logger:info("[install] Raw SID: " .. sid)
-- SID format: S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXX
-- Remove the last RID component (as extloader does)
if sys_utils.startswith(sid, "S-1-") then
local parts = sys_utils.split(sid, "-")
-- Remove last element
table.remove(parts)
sid = sys_utils.join(parts, "-")
logger:info("[install] SID (without RID): " .. sid)
return sid
end
logger:warn("[install] SID doesn't match expected format")
return ""
end
return M