mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
FIX - complete LB custom app compatibility (#11)
* FIX - preserve LB custom app lifecycle * FIX - reset LB export aliases before startup * FIX - report competing custom app providers * FIX - identify LB app frames as live NUI * FIX - complete LB custom app compatibility * FIX - provide LB PicChat runtime exports * FIX - route LB PicChat through Sky Phone * FIX - return cached LB equipped phone number * FIX - harden PicChat compatibility migration * ENH - complete modular phone provider and Creator APIs * DOC - document the Sky Phone Creator API --------- Co-authored-by: DerEchteAlec <bycraky@gmail.com> Co-authored-by: DerEchteAlec <alec.schitzkat@luwan.io>
This commit is contained in:
co-authored by
DerEchteAlec
DerEchteAlec
parent
9aec3eefef
commit
30559048fd
@@ -0,0 +1,218 @@
|
||||
Bridge.Database.AfterMigration("sky_phone", function()
|
||||
SkyPhoneSecurity = {}
|
||||
|
||||
local passcode_pepper = tostring(Config.Server.PasscodePepper or "")
|
||||
if passcode_pepper == "" then
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
"[sky_phone] Config.Server.PasscodePepper is empty. Device passcodes still work, but their hashes lack the required server-side secret. Set a stable random value in config/config.lua before production; changing it later invalidates existing device passcodes.",
|
||||
{ always = true }
|
||||
)
|
||||
end
|
||||
|
||||
local function affected_rows(result)
|
||||
if type(result) == "number" then
|
||||
return result
|
||||
end
|
||||
|
||||
return type(result) == "table" and tonumber(result.affectedRows) or 0
|
||||
end
|
||||
|
||||
local function load_device_security(imei)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT `passcode_length`, `failed_attempts`, `locked_until`
|
||||
FROM `sky_phone_device_security`
|
||||
WHERE `device_imei` = ?
|
||||
LIMIT 1
|
||||
]], { imei })
|
||||
return rows[1]
|
||||
end
|
||||
|
||||
local function valid_passcode(value)
|
||||
return type(value) == "string"
|
||||
and (#value == 4 or #value == 6)
|
||||
and value:match("^%d+$") ~= nil
|
||||
end
|
||||
|
||||
local function passcode_matches(imei, passcode)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT 1 AS `matches`
|
||||
FROM `sky_phone_device_security`
|
||||
WHERE `device_imei` = ?
|
||||
AND `passcode_hash` = UNHEX(SHA2(CONCAT(?, `passcode_salt`, ?), 256))
|
||||
LIMIT 1
|
||||
]], { imei, passcode_pepper, passcode })
|
||||
return rows[1] ~= nil
|
||||
end
|
||||
|
||||
local function verify_passcode(session, passcode)
|
||||
if not valid_passcode(passcode) then
|
||||
return false, { success = false, error = "invalid_passcode" }
|
||||
end
|
||||
|
||||
local security = load_device_security(session.imei)
|
||||
if not security then
|
||||
return false, { success = false, error = "passcode_not_set" }
|
||||
end
|
||||
|
||||
local now = os.time()
|
||||
local locked_until = tonumber(security.locked_until) or 0
|
||||
if locked_until > now then
|
||||
return false, {
|
||||
success = false,
|
||||
error = "passcode_locked",
|
||||
data = { retryAfter = locked_until - now },
|
||||
}
|
||||
end
|
||||
|
||||
if passcode_matches(session.imei, passcode) then
|
||||
Bridge.Database.Query([[
|
||||
UPDATE `sky_phone_device_security`
|
||||
SET `failed_attempts` = 0, `locked_until` = 0
|
||||
WHERE `device_imei` = ?
|
||||
]], { session.imei })
|
||||
return true
|
||||
end
|
||||
|
||||
local failed_attempts = (tonumber(security.failed_attempts) or 0) + 1
|
||||
if failed_attempts >= Config.Security.MaximumAttempts then
|
||||
local next_unlock = now + Config.Security.LockSeconds
|
||||
Bridge.Database.Query([[
|
||||
UPDATE `sky_phone_device_security`
|
||||
SET `failed_attempts` = 0, `locked_until` = ?
|
||||
WHERE `device_imei` = ?
|
||||
]], { next_unlock, session.imei })
|
||||
return false, {
|
||||
success = false,
|
||||
error = "passcode_locked",
|
||||
data = { retryAfter = Config.Security.LockSeconds },
|
||||
}
|
||||
end
|
||||
|
||||
Bridge.Database.Query([[
|
||||
UPDATE `sky_phone_device_security`
|
||||
SET `failed_attempts` = ?
|
||||
WHERE `device_imei` = ?
|
||||
]], { failed_attempts, session.imei })
|
||||
return false, {
|
||||
success = false,
|
||||
error = "invalid_passcode",
|
||||
data = { attemptsRemaining = Config.Security.MaximumAttempts - failed_attempts },
|
||||
}
|
||||
end
|
||||
|
||||
function SkyPhoneSecurity.Load(imei)
|
||||
return load_device_security(imei)
|
||||
end
|
||||
|
||||
function SkyPhoneSecurity.Status(imei, security, security_loaded)
|
||||
if not security_loaded then
|
||||
security = load_device_security(imei)
|
||||
end
|
||||
return {
|
||||
enabled = security ~= nil,
|
||||
length = security and tonumber(security.passcode_length) or nil,
|
||||
lockedUntil = security and tonumber(security.locked_until) or 0,
|
||||
}
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:security:unlock", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "security_unlock", Config.Security.AttemptsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local session, error_response = SkyPhone.RequireDeviceSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
if session.unlocked then
|
||||
return { success = true, data = { security = SkyPhoneSecurity.Status(session.imei) } }
|
||||
end
|
||||
|
||||
local verified, verification_error = verify_passcode(session, data and data.passcode)
|
||||
if not verified then
|
||||
return verification_error
|
||||
end
|
||||
session.unlocked = true
|
||||
return { success = true, data = { security = SkyPhoneSecurity.Status(session.imei) } }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:security:set-passcode", function(source, data)
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
local passcode = data and data.passcode
|
||||
if not valid_passcode(passcode) then
|
||||
return { success = false, error = "invalid_passcode" }
|
||||
end
|
||||
if load_device_security(session.imei) then
|
||||
return { success = false, error = "passcode_already_set" }
|
||||
end
|
||||
|
||||
local salts = Bridge.Database.Query("SELECT REPLACE(UUID(), '-', '') AS `salt`", {})
|
||||
local salt = salts[1] and salts[1].salt
|
||||
if type(salt) ~= "string" or #salt ~= 32 then
|
||||
error("[sky_phone] Database did not generate a valid passcode salt.")
|
||||
end
|
||||
local result = Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_device_security`
|
||||
(`device_imei`, `passcode_hash`, `passcode_salt`, `passcode_length`)
|
||||
VALUES (?, UNHEX(SHA2(CONCAT(?, ?, ?), 256)), ?, ?)
|
||||
]], { session.imei, passcode_pepper, salt, passcode, salt, #passcode })
|
||||
if affected_rows(result) ~= 1 then
|
||||
return { success = false, error = "request_failed" }
|
||||
end
|
||||
return { success = true, data = { security = SkyPhoneSecurity.Status(session.imei) } }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:security:change-passcode", function(source, data)
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
local new_passcode = data and data.newPasscode
|
||||
if not valid_passcode(new_passcode) then
|
||||
return { success = false, error = "invalid_passcode" }
|
||||
end
|
||||
local verified, verification_error = verify_passcode(session, data and data.currentPasscode)
|
||||
if not verified then
|
||||
return verification_error
|
||||
end
|
||||
|
||||
local salts = Bridge.Database.Query("SELECT REPLACE(UUID(), '-', '') AS `salt`", {})
|
||||
local salt = salts[1] and salts[1].salt
|
||||
if type(salt) ~= "string" or #salt ~= 32 then
|
||||
error("[sky_phone] Database did not generate a valid passcode salt.")
|
||||
end
|
||||
local result = Bridge.Database.Query([[
|
||||
UPDATE `sky_phone_device_security`
|
||||
SET `passcode_hash` = UNHEX(SHA2(CONCAT(?, ?, ?), 256)),
|
||||
`passcode_salt` = ?, `passcode_length` = ?, `failed_attempts` = 0, `locked_until` = 0
|
||||
WHERE `device_imei` = ?
|
||||
]], { passcode_pepper, salt, new_passcode, salt, #new_passcode, session.imei })
|
||||
if affected_rows(result) ~= 1 then
|
||||
return { success = false, error = "request_failed" }
|
||||
end
|
||||
return { success = true, data = { security = SkyPhoneSecurity.Status(session.imei) } }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:security:disable-passcode", function(source, data)
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
local verified, verification_error = verify_passcode(session, data and data.passcode)
|
||||
if not verified then
|
||||
return verification_error
|
||||
end
|
||||
local result = Bridge.Database.Query(
|
||||
"DELETE FROM `sky_phone_device_security` WHERE `device_imei` = ?",
|
||||
{ session.imei }
|
||||
)
|
||||
if affected_rows(result) ~= 1 then
|
||||
return { success = false, error = "request_failed" }
|
||||
end
|
||||
session.unlocked = true
|
||||
return { success = true, data = { security = SkyPhoneSecurity.Status(session.imei) } }
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user