adds .local/share/millennium and easyeffects
This commit is contained in:
@@ -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
|
||||
+62
@@ -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
|
||||
Reference in New Issue
Block a user