API 레퍼런스
OZeroSecurity SDK의 모든 공개 클래스와 메서드에 대한 완전한 레퍼런스입니다. 별도로 명시되지 않은 경우 모든 클래스는 OZeroSDK.Security 네임스페이스에 있습니다.
OZeroSecurityManager OZeroSDK.Security
활성화된 보안 모듈과 보안 이벤트를 관리하는 전역 관리자입니다. 씬이 바뀌어도 유지되며, Instance 프로퍼티로 접근할 수 있습니다. SDK가 앱 시작 시 자동으로 생성하므로 직접 인스턴스를 만들 필요는 없습니다.
프로퍼티
| 이름 | 타입 | 설명 |
|---|---|---|
| Instance | OZeroSecurityManager | 정적 싱글톤 접근자. 활성 인스턴스를 반환합니다. |
메서드
보안 이벤트를 프로젝트 코드에서 받아보고 싶을 때 콜백을 등록합니다. OZero의 기본 대응은 별도 흐름에서 계속 실행되므로, 이 콜백을 등록하거나 해제해도 SDK 보호 동작이 꺼지지 않습니다. 콜백에는 탐지 영역, 공개 중단 코드, 메시지 키, 진단 메시지, 앱 종료 예정 여부가 담긴 OZeroSecurityEvent가 전달됩니다.
이전에 등록한 유저 콜백을 제거합니다. 메모리 누수 방지를 위해 OnDisable 또는 OnDestroy에서 반드시 호출하세요.
CustomCallbackOnly에서 사용할 프로젝트 콜백을 등록합니다. 이 콜백은 네이티브 코드가 되돌릴 수 없는 치명적 종료 시한을 설정한 뒤에만 호출됩니다. 알림을 표시하거나 짧은 정리 작업을 할 수 있지만 종료를 취소하거나 시한을 연장할 수 없습니다.
등록한 보안 종료 알림 콜백을 제거합니다.
이미 대기 중인 치명적 보안 종료를 즉시 완료합니다. 일반적으로 알림 창의 확인 버튼에서 호출하며, 대기 중인 종료가 없으면 아무 작업도 하지 않습니다.
Pro 기기 정책 heartbeat를 통해 서버에서 내려온 정책 액션을 받을 콜백을 등록합니다. 포털에서 Callback 대응 정책을 사용할 때 게임 코드가 후속 처리를 하도록 연결하는 용도이며, 로컬 보안 탐지 콜백과는 별도로 동작합니다.
이전에 등록한 정책 액션 콜백을 제거합니다. 씬 오브젝트에서 사용할 때는 OnEnable에서 등록하고 OnDisable에서 해제하세요.
사용자에게 보여 줄 활성화 또는 DRM 상태 변화를 받을 콜백을 등록합니다. OZero Managed Steam DRM과 기본 Managed Verification UI를 함께 쓰면 SDK가 온라인 필요, 재시도, timeout, 차단 상태를 이미 표시합니다. 커스텀 UI, telemetry, 게임 전용 session gate가 필요할 때만 등록하세요.
이전에 등록한 활성화 상태 콜백을 제거합니다. 씬 오브젝트에서 사용할 때는 OnEnable에서 등록하고 OnDisable에서 해제하세요.
Registers a callback for the shared OZero Managed Verification state used by Pro Build Integrity and Steam DRM. The default SDK UI handles these states automatically when enabled; use this callback only for custom UI, custom telemetry, or game-specific session gates.
Removes a previously registered managed verification state callback. Register in OnEnable and unregister in OnDisable when using a scene object.
Replaces individual built-in verification UI strings at runtime. Use JSON files under Assets/OZeroSecurity/Resources/OZeroLocalization for normal localization, and use this provider only when text must be supplied dynamically.
Removes a previously registered managed verification text provider.
Retries SDK-managed Build Integrity attestation after an online-required or retry-timeout state. The default Managed Verification UI calls this automatically. If your own game server validates OZA tokens, retry your own login or session-refresh request instead.
OZero 서버가 직접 처리하는 Steam Activation / DRM 검증을 다시 시도합니다. 기본 Managed Verification UI는 필요한 retry 흐름을 SDK 안에서 호출합니다. 기본 UI를 의도적으로 대체한 커스텀 UI에서만 게임 코드가 이 메서드를 호출하세요. 고객 게임 서버가 Steam 인증을 직접 처리하는 구조라면 이 메서드가 아니라 고객 서버의 로그인/세션 요청을 다시 보내야 합니다.
델리게이트
RegisterUserCallback에서 사용하는 콜백 시그니처입니다. 경고 UI, 자체 서버 로그, 짧은 저장 처리가 필요할 때 evt.Type, evt.AbortCodeHex, evt.MessageKey, evt.Message, evt.WillAbort를 확인하세요. evt.WillAbort가 true이면 앱 종료가 예정되어 있으므로 오래 걸리는 작업은 피해야 합니다.
예시
using OZeroSDK.Security;
using UnityEngine;
public class MySecurityListener : MonoBehaviour
{
void OnEnable()
=> OZeroSecurityManager.Instance.RegisterUserCallback(OnThreat);
void OnDisable()
=> OZeroSecurityManager.Instance.UnregisterUserCallback(OnThreat);
void OnThreat(OZeroSecurityEvent evt)
=> Debug.Log(
$"Threat={evt.Type}, Code={evt.AbortCodeHex}, Message={evt.Message}");
}
OZeroSecurityTerminationNoticeEvent class
네이티브 치명적 종료 시한이 설정된 뒤에만 전달되는 사용자용 페이로드입니다. 원래의 안전한 보안 이벤트와 고객 지원에 전달할 수 있는 안정적인 지원 코드를 포함합니다.
| 이름 | 타입 | 설명 |
|---|---|---|
| SecurityEvent | OZeroSecurityEvent | 원래의 안전한 이벤트입니다. WillAbort는 true이며 로그와 현지화에는 AbortCode와 MessageKey를 사용합니다. |
| SupportCode | string | 플레이어가 고객 지원에 전달할 수 있는 안정적인 비민감 코드입니다. |
UI 표시나 콜백 호출은 네이티브 치명적 종료 시한을 일시 중지, 취소 또는 연장하지 않습니다. 모든 작업을 짧게 제한하고 더 일찍 종료할 때만
OZeroSecurityManager.ExitNow()를 호출하세요.OZeroSecurityEvent class
RegisterUserCallback에 전달되는 고객 공개용 위반 이벤트입니다. 내부 탐지 세부 정보 대신 안정적이고 안전한 진단 정보만 노출합니다.
| 이름 | 타입 | 설명 |
|---|---|---|
| Type | ModulationType | 위반을 발생시킨 보안 모듈입니다. |
| AbortCode | OZeroAbortCode | 안정적인 공개 중단 코드 카테고리입니다. |
| AbortCodeValue | int | 서버 로그에 사용하기 좋은 숫자 코드 값입니다. |
| AbortCodeHex | string | 0x0C 같은 16진수 문자열입니다. |
| MessageKey | string | 현지화와 analytics 그룹화에 사용할 수 있는 안정적인 영문 메시지 키입니다. |
| Message | string | 고객에게 노출 가능한 안전한 영문 진단 메시지입니다. |
| WillAbort | bool | 콜백 반환 후 또는 유예 시간이 끝난 뒤 현재 대응 정책이 앱을 종료할 예정이면 true입니다. |
OZeroPolicyActionEvent class
RegisterUserPolicyActionCallback으로 전달되는 서버 정책 콜백 페이로드입니다. 고객 포털 정책 시스템에서 발행되며, Pro 기기 정책 heartbeat를 통해 Unity 메인 스레드에서 전달됩니다.
| 이름 | 타입 | 설명 |
|---|---|---|
| Module | string | variant, injection, physics, user_policy(사용자 UUID 차단 규칙. 강제 종료 규칙으로 세션이 끝나기 직전에는 ActionId = "user_blocked") 같은 정책 모듈입니다. |
| ActionId | string | 전달 건을 식별하는 고유 ID입니다. SDK는 이 값으로 중복 전달을 막고 서버에 ack를 보냅니다. |
| PolicyId | string | 가능한 경우 고객 포털 정책 ID입니다. |
| Reason | string | 정책에 설정된 사유 또는 매칭된 증거에서 생성된 사유입니다. |
| Score | int | 정책이 매칭된 시점의 위험 점수입니다. |
| EventCount | int | 정책 매칭에 반영된 이벤트 수입니다. |
| WindowMinutes | int | 정책 평가 윈도우(분)입니다. |
| IssuedAtUnixMs | long | 서버가 액션을 발행한 UTC epoch milliseconds입니다. |
| ExpiresAtUnixMs | long | ack되지 않은 액션이 만료되는 UTC epoch milliseconds입니다. |
정책 콜백 예시
고객 포털 보안 정책의 대응을 Callback으로 설정했을 때 사용합니다. SDK는 정책 액션을 게임으로 전달하고, 게임은 사용자 안내, 기능 제한, 화면 이동을 직접 결정합니다.
using OZeroSDK.Security;
using UnityEngine;
public sealed class OZeroPolicyListener : MonoBehaviour
{
private void OnEnable()
=> OZeroSecurityManager.Instance.RegisterUserPolicyActionCallback(OnPolicyAction);
private void OnDisable()
=> OZeroSecurityManager.Instance.UnregisterUserPolicyActionCallback(OnPolicyAction);
private void OnPolicyAction(OZeroPolicyActionEvent evt)
{
Debug.LogWarning($"Policy={evt.PolicyId}, Module={evt.Module}, Score={evt.Score}, Reason={evt.Reason}");
if (evt.Score >= 80)
{
ShowSecurityNotice(evt.Reason);
DisableRankedMatchmaking();
}
}
}
OZeroUserActivationStateEvent class
RegisterUserActivationStateCallback으로 전달되는 사용자 표시용 활성화 또는 DRM 상태 페이로드입니다. Steam Activation / DRM은 캐시된 오프라인 실행이 아직 허용되는지, 온라인 재검증이 필요한지를 이 이벤트로 게임에 알려줍니다.
| 이름 | 타입 | 설명 |
|---|---|---|
| Provider | string | steam 같은 활성화 제공자입니다. |
| State | string | valid, revalidated, online_required, offline_degraded, rejected 같은 안정적인 상태값입니다. |
| Reason | string | steam_activation_revalidate_unavailable, steam_activation_cache_expired 같은 사유 코드입니다. |
| Action | string | 가능한 경우 이 상태와 연결된 포털 정책 액션 값입니다. |
| OfflineAllowed | bool | 서명이 검증된 토큰이 오프라인 허용 시간 안에 있을 때만 true입니다. |
| OnlineRequired | bool | 보호 세션을 허용하기 전에 사용자에게 네트워크 연결과 재검증을 요구해야 하면 true입니다. |
| RefreshAtUnixMs | long | 온라인 갱신이 필요한 UTC epoch milliseconds입니다. |
| OfflineUntilUnixMs | long | 서명된 오프라인 허용 시간이 끝나는 UTC epoch milliseconds입니다. |
활성화 상태 콜백 사용 기준
OZero Managed Steam DRM과 기본 Managed Verification UI를 함께 쓰면 activation callback 코드는 필요하지 않습니다. SDK가 플레이어에게 온라인 필요, 재시도, timeout, 차단 상태를 표시합니다. 이 콜백은 기본 UI를 의도적으로 대체하거나, 별도 session gate를 붙이거나, 고객 게임 서버 검증 흐름을 운영할 때만 등록하세요. Steam Web API Key나 OZero Server API Key를 클라이언트에 넣지 마세요.
OZeroUserManagedVerificationStateEvent class
RegisterUserManagedVerificationStateCallback으로 전달되는 사용자 표시용 managed verification 페이로드입니다. Build Integrity와 Steam DRM이 모두 이 이벤트를 사용하지만, 기본 Managed Verification UI를 쓰면 SDK가 자동으로 소비합니다. 게임 코드에서는 커스텀 UI, 별도 telemetry, 게임 전용 session gate가 필요할 때만 처리하세요.
| 이름 | 타입 | 설명 |
|---|---|---|
| Provider | OZeroManagedVerificationProvider | Source module: BuildIntegrity or SteamDrm. |
| State | OZeroUserManagedVerificationState | Current state such as Checking, Allowed, Warning, OnlineRequired, RetryTimedOut, Blocked, or StandardFallback. |
| Reason | string | Stable reason code suitable for custom UI text, analytics, or customer support logs. |
| Verdict | string | Server verdict when available, such as allow, warn, block, or fallback. |
| CanRetry | bool | True when a Retry action is meaningful for the current provider and state. |
| TimestampUnixMs | long | UTC epoch milliseconds when the state was raised. |
Managed verification 콜백 사용 기준
Security Config > Pro - License & Online Services > Online Verification UI > Ui Policy가 OZero 기본 다이얼로그를 사용하면 SDK가 Build Integrity와 Steam DRM의 managed-verification UI, 재시도, timeout 상태를 이미 처리합니다. RegisterUserManagedVerificationStateCallback은 Custom UI / Callback Only, 별도 telemetry, 게임 전용 session gate에만 등록하세요. 커스텀 UI 모드에서는 Provider에 맞는 retry 메서드를 호출하고, 고객 게임 서버 모드에서는 SDK retry가 아니라 자체 로그인/세션 요청을 다시 보내세요.
OZeroManagedVerificationUiPolicy enum
Build Integrity와 Steam DRM에서 발생하는 managed verification 상태를 SDK가 플레이어에게 어떻게 표시할지 결정합니다. 두 Built-In 정책에는 TextMeshPro, TMP Essential Resources, optional OZero Built-In TMP Dialog package가 필요합니다. 이 조건 없이 Built-In 정책을 선택하면 build preflight가 player build를 중단합니다. Window > OZero Security > Check Setup에서 해당 오류의 Import Package를 눌러 프로젝트를 수정하세요.
| 값 | 설명 |
|---|---|
| CustomCallbackOnly | SDK는 RegisterUserManagedVerificationStateCallback 상태만 전달합니다. 게임이 자체 UI, 재시도 버튼, timeout 처리, session gate를 모두 직접 구현할 때 사용합니다. |
| BuiltInBlockingDialog | OZero 기본 차단형 다이얼로그를 사용합니다. 온라인 재검증이 필요한 동안 보호 세션을 멈추거나 차단해야 하는 출시 빌드의 기본 권장값입니다. |
| BuiltInNonBlockingDialog | OZero 기본 안내형 다이얼로그를 사용합니다. 재시도나 경고 상태를 안내하는 동안에도 게임 진행이 안전하게 가능할 때만 사용하세요. |
OZeroSecurityTerminationNoticePolicy enum
이미 설정된 치명적 보안 종료 직전에 표시할 선택적 사용자 알림을 제어합니다. Standard / Pro 라이선스에서 공통으로 사용할 수 있습니다.
| 값 | 설명 |
|---|---|
| Disabled = 0 | 종료 알림을 표시하지 않습니다. 네이티브 치명적 종료 시한은 그대로 적용됩니다. |
| OZeroBuiltIn = 1 | 현지화된 OZero 기본 대화상자를 표시하고 플레이어가 확인하면 즉시 종료합니다. |
| CustomCallbackOnly = 2 | 등록된 종료 알림 콜백을 호출하여 프로젝트가 사용자 UI를 표시하게 합니다. 종료를 취소하거나 연장할 수 없습니다. |
OZeroManagedVerificationTimeoutAction enum
온라인 필요 또는 재시도 상태가 설정된 제한 시간 안에 해결되지 않았을 때 기본 managed verification UI가 취할 동작을 정합니다.
| 값 | 설명 |
|---|---|
| KeepDialog | timeout 이후에도 다이얼로그를 계속 표시합니다. 플레이어가 계속 수동으로 재시도해야 하는 흐름에서만 선택하세요. |
| BlockSession | timeout 이후 보호 세션을 차단합니다. 온라인 검증은 필수지만 애플리케이션은 유지해야 하는 경우의 기본 권장값입니다. |
| AbortApplication | timeout 이후 애플리케이션을 종료합니다. 해결되지 않은 검증 실패 시 즉시 종료해야 하는 출시 정책에서만 사용하세요. |
| InvokeCallbackOnly | 기본 차단 또는 종료 동작 없이 콜백만 호출합니다. 완전한 커스텀 세션 제어가 필요한 경우에 사용합니다. |
ModulationType enum
어떤 보안 모듈이 경보를 발생시켰는지 식별합니다. OZeroSecurityEvent.Type으로 전달됩니다.
| 값 | 설명 |
|---|---|
| MemoryModulation | Secure Type 변수가 의심스러운 방식으로 접근됨 |
| SpeedHack | 스피드핵 또는 시간 조작 감지 |
| TimeHack | 시스템 시계 이상 감지 (뒤로 이동, NTP 불일치) |
| Injection | 메모리 인젝션 툴(Frida 등) 또는 불법 DLL 감지 |
| PhysicsHack | 불가능한 위치 변화 감지 (OZeroPhysicsHackDetector에서 발생 — 플레이어 오브젝트에 직접 컴포넌트를 붙이고 초기화해야 합니다.) |
| DeviceBindingModulation | 세이브 데이터가 바인딩된 기기와 다른 기기에서 로드됨 |
| InstallSource | 인가된 스토어가 아닌 곳에서 설치됨 |
| BuildIntegrity | 어셈블리 해시 불일치, 디버거 연결, 또는 플랫폼 검사 실패 |
| EnvironmentModulation | 에뮬레이터 또는 비정상 런타임 환경 감지 |
| SteamAntiPiracy | Steam 소유권 또는 티켓 검증에 실패했을 때 발생합니다. |
| InjectionScannerUnavailable | 네이티브 인젭션 스캐너가 동작하지 못함(fail-close; abort code 0x15, 공격 탐지 아님) |
OZeroBootstrapper OZeroSDK.Security
SDK 초기화를 담당하는 자동 시작 진입점입니다. 프로젝트 코드에서 직접 호출할 필요는 없습니다. Unity 시작 흐름에 맞춰 보안 설정을 로드하고, 게임 시작 전에 활성화된 탐지기를 준비합니다.
OZeroSecurityConfig, Project Settings에서 편집)뿐입니다.
OZeroSecurityConfigRuntime OZeroSDK.Security
보호된 빌드 설정을 플레이어 실행 시 읽고 검증하는 구성 로더입니다. 패키지된 설정을 검증한 뒤 OZeroSecurityConfig 실행 설정을 준비하며, 검증 실패 시 설정된 위협 대응 정책을 적용합니다.
프로퍼티
| 이름 | 타입 | 설명 |
|---|---|---|
| Current | OZeroSecurityConfig | 플레이어 빌드에 포함된 보호 설정을 읽어 만든 설정 스냅샷입니다. 처음 접근할 때 EnsureLoaded()를 호출합니다. |
메서드
반복 호출해도 안전한 로더입니다. 첫 호출에서 패키지된 설정을 검증하고 로드하며, 이후 호출은 같은 설정 스냅샷을 재사용합니다. 실패 시에는 설정된 대응 정책을 따릅니다.
OZeroSecurityConfig와 Unity 에디터 창으로 보시면 됩니다.
OZero Secure Variables OZeroSDK.Security
일반 숫자, 문자열, 벡터 타입 대신 사용할 수 있는 암호화 타입입니다. 값은 보호된 메모리 영역에 저장되며, 대부분의 산술 연산자와 암묵적 변환을 지원합니다. 기존 코드에서 타입 이름만 바꿔 적용할 수 있습니다.
지원 타입
| 클래스 | 대체 타입 |
|---|---|
| OZeroSV_Int | int |
| OZeroSV_Int64 | long |
| OZeroSV_UInt | uint |
| OZeroSV_UInt64 | ulong |
| OZeroSV_Short | short |
| OZeroSV_UShort | ushort |
| OZeroSV_Byte | byte |
| OZeroSV_Float | float |
| OZeroSV_Double | double |
| OZeroSV_Decimal | decimal |
| OZeroSV_Bool | bool |
| OZeroSV_String | string |
| OZeroSV_Vector2 | Vector2 |
| OZeroSV_Vector3 | Vector3 |
| OZeroSV_Buffer | byte[] |
지원 연산자
숫자 타입(Int, Int64, UInt, UInt64, Short, UShort, Byte, Float, Double, Decimal)은 산술(+ - * / %), 비교(== != < > <= >=), 복합 대입(+= -= *= /=), 증감(++ --) 연산자와 기본 타입과의 암묵적 변환을 지원합니다. Vector2·Vector3는 산술 및 동등 연산자를 지원하고, Bool은 동등 연산자만 지원합니다. String은 ==, !=, +를 지원합니다. Buffer는 인덱스 연산자를 통해 바이트 배열에 직접 접근할 수 있습니다.
OZeroSafePlayerPrefs OZeroSDK.Security
Unity PlayerPrefs와 비슷한 방식으로 사용할 수 있는 암호화 저장소입니다. 키 이름과 값이 보호되어 Windows 레지스트리나 iOS 설정 파일을 직접 열어도 원래 값을 읽기 어렵습니다.
메서드
자주 쓰는 PlayerPrefs 스타일 메서드에 더해 Int64, Double, Bool, IncrementInt 헬퍼를 제공합니다. 기존 plain PlayerPrefs 값이 자동으로 이전되지는 않으므로, 앞으로 보호할 key부터 OZeroSafePlayerPrefs로 저장하세요.
OZeroSafePlayerPrefs로 쓴 데이터는 표준 PlayerPrefs와 호환되지 않습니다. 두 가지를 전환하면 기존 데이터를 읽을 수 없게 됩니다.
OZeroSV_File OZeroSDK.Security
내부 암호화 로직으로 파일 읽기와 쓰기를 보호합니다. 기기 바인딩 키를 사용하지 않으므로 스팀 클라우드 세이브처럼 여러 기기에서 같은 세이브 파일을 읽어야 하는 경우에도 사용할 수 있습니다. 파일을 고의로 변조하면 읽는 시점에 무결성 검사가 실패하고 InvalidDataException이 발생합니다.
OZeroSV_File API를 통해 처리하세요.
메서드
contents를 암호화해 path에 저장합니다. 상위 폴더는 자동으로 생성하지 않으므로, 필요한 경우 저장 전에 Directory.CreateDirectory로 먼저 만들어 주세요.
path의 파일을 읽고 무결성을 검증한 후 복호화된 문자열을 반환합니다. 파일이 변조된 경우 InvalidDataException을 발생시킵니다.
데이터를 암호화하고 path에 안전하게 저장합니다.
path에 지정된 파일을 읽습니다. 읽기 시 파일 무결성을 검증하며 변조 여부를 탐지합니다.
파일 경로가 아니라 이미 메모리에 올라온 암호화 바이트 버퍼를 복호화합니다. 원격 다운로드나 커스텀 저장소에서 받은 데이터를 ReadAllText에 넘길 수 없을 때 사용하세요.
예시 코드
using OZeroSDK.Security;
string path = Application.persistentDataPath + "/save.json";
string json = JsonUtility.ToJson(saveData);
// Write (encrypts automatically)
OZeroSV_File.WriteAllText(path, json);
// Read (decrypts + integrity check)
try
{
string loaded = OZeroSV_File.ReadAllText(path);
saveData = JsonUtility.FromJson<SaveData>(loaded);
}
catch (System.IO.InvalidDataException)
{
// File was tampered — handle accordingly
Debug.LogError("Save file integrity check failed.");
}
OZeroBuildIntegrityValidator OZeroSDK.Security
빌드 변조, 디버거/타이밍 이상, 플랫폼 네이티브 무결성 검사, 선택적 Pro 서버 검증을 처리하는 검증기입니다. Build Integrity가 OZeroSecurityConfig에서 활성화되면 SDK가 자동으로 생성합니다.
검사 항목
| 검사 | 설명 |
|---|---|
| Assembly / Manifest | 지원 빌드 타겟에서 생성된 무결성 매니페스트와 managed assembly 상태를 검증합니다. |
| Debugger / Timing | 연결된 디버거, 비정상 타이밍 간격, breakpoint에 가까운 지연을 감지하며 일반적인 포커스 손실 오탐은 억제합니다. |
| Platform Native | 활성화 시 Android 패키지/서명, iOS jailbreak, 데스크톱 런타임 상태 같은 플랫폼별 검사를 수행합니다. |
| Pro Attestation | Pro 서버 검증이 켜져 있으면 로컬 검사 통과 후 서버에서 검증 토큰을 요청합니다. 자체 게임 서버가 없는 경우 OZero 서버가 허용/경고/차단 결과도 함께 반환할 수 있습니다. |
공개 속성
| 이름 | 타입 | 설명 |
|---|---|---|
| Instance | OZeroBuildIntegrityValidator | 모듈이 생성된 경우 현재 validator 인스턴스입니다. |
| LastValidationResult | bool? | 가장 최근 로컬 검증 결과입니다. 첫 검증 전에는 null입니다. |
| IsValidating | bool | 검증 실행 중이면 true입니다. |
| IsIntegrityVerified | bool | 최근 활성화된 로컬 검사를 통과하면 true입니다. |
| OZeroOnlineAttestation.Token | OZeroBuildAttestationToken | 가장 최근 Pro attestation 토큰입니다. 서버 attestation이 성공하거나 실패하기 전까지는 null이며, 토큰에는 재사용 추적을 위한 고유 ID가 포함됩니다. |
이벤트 및 메서드
활성화된 모든 로컬 검사가 통과하면 호출됩니다.
활성화된 로컬 검사 또는 Pro attestation이 빌드를 거부하면 호출됩니다.
온라인 애드온에서는 OZeroOnlineAttestation.OnAttestationPassed입니다. 서버 어테스테이션이 성공하고 OZeroOnlineAttestation.Token에 유효한 토큰이 있을 때 호출됩니다.
온라인 애드온에서는 OZeroOnlineAttestation.RequestGameServerAttestation으로 옮겨졌습니다. CustomerGameServer 전용이며, 로컬 검사가 통과한 뒤 서버가 발급한 audience, hex 64자 challenge, session ID로 어테스테이션을 시작합니다. 토큰은 OZeroOnlineAttestation.TokenReady로 도착합니다. 서버 API 키로 게임 서버 연동하기를 참고하세요.
수동 검증을 시작합니다. 일반 프로젝트는 대시보드의 시작 시/주기적 검증 설정을 사용하는 편이 좋습니다.
OZeroBuildAttestationToken
Pro attestation 결과입니다. AttestToken을 게임 서버로 전달하고, 로그인, PvP, 랭킹, 재화 처리 같은 흐름에 사용하기 전에는 IsValid(nowMillis)로 유효성을 확인하세요.
서버가 발급한 만료 시간이 지난 경우 true를 반환합니다.
토큰 발급이 성공했고 아직 만료되지 않은 경우 true를 반환합니다.
OZeroSpeedHackDetector OZeroSDK.Security
5가지 독립적인 감지 신호를 사용하여 스피드핵과 시간 조작을 감지합니다. 신호들이 서로 확인할 때만 위협을 보고하여 오탐을 줄입니다.
감지 신호
| 신호 | 설명 |
|---|---|
| TimeScale | 게임 시간 흐름이 비정상적으로 바뀌는지 확인 |
| API Clock | 플랫폼 시간과 네이티브 기준 시간을 비교해 큰 차이를 감지 |
| Thread Drift | Unity 런타임 시간과 네이티브 기준 시간의 흐름 차이를 관찰 |
| Time Backward | 기기 시간이 비정상적으로 되돌아가는 상황을 감지 |
| NTP | 선택적 — 신뢰할 수 있는 외부 시간 기준과 비교(네트워크 필요) |
감지는 ModulationType.SpeedHack 또는 ModulationType.TimeHack으로 OZeroSecurityManager 콜백을 통해 발생합니다. OZeroSecurityConfig에서 설정합니다.
OZeroWatchdog OZeroSDK.Security
신뢰할 수 있는 장시간 로딩 작업을 위한 public helper입니다. 릴리스 deadline보다 오래 Unity 메인 스레드를 막을 수 있는 동기 작업 구간에서 native Watchdog heartbeat deadline을 제한적으로 유예합니다.
메서드
제한된 loading grace scope를 시작합니다. 신뢰할 수 있는 로딩 작업이 끝나는 즉시 반환된 scope의 End()를 호출하거나 dispose하세요. 중첩 scope를 지원하며, 마지막 scope가 끝나면 정상 Watchdog 타이밍으로 돌아갑니다.
이 loading grace scope를 수동으로 종료합니다. Dispose()도 같은 로직을 호출하므로 using 블록과 명시적 End()는 동일하게 동작합니다.
동기 로딩 작업을 위한 편의 wrapper입니다. loading grace scope를 만들고 work를 실행한 뒤, using 블록으로 scope를 안전하게 종료합니다.
예시
using OZeroSDK.Security;
using UnityEngine.SceneManagement;
public void LoadLargeScene()
{
using (OZeroWatchdog.BeginLoadingGrace(60000))
{
SceneManager.LoadScene("Battle", LoadSceneMode.Single);
}
}
OZeroInjectionDetector OZeroSDK.Security
실행 중인 게임에 의심스러운 모듈이 붙었는지, 후킹이나 디버거 흔적이 있는지 확인합니다. 주기 검사는 가능한 경우 검사 타이밍을 조금씩 바꿔 단순한 우회 시도를 어렵게 만듭니다.
감지 대상
| Runtime module | 예상치 못한 런타임 모듈 또는 후킹 의심 신호 |
| Debugger | 디버깅 또는 추적 도구 연결 의심 신호 |
| Memory map | 비정상적인 런타임 메모리 또는 모듈 상태 신호 |
| Illegal DLL | 프로세스에 로드된 허용되지 않은 관리 어셈블리 신호(Windows/Unity Editor) |
감지는 ModulationType.Injection으로 OZeroSecurityManager 콜백을 통해 발생합니다.
OZeroPCStoreAntiPiracy OZeroSDK.Security
Steam Anti-Piracy의 감지 후 동작을 실행 중에 바꾸는 API입니다. 대부분의 프로젝트는 Config Dashboard에서 설정하면 충분합니다. 게임 안에 운영자용 메뉴나 QA용 스위치를 직접 제공하는 경우에만 사용하세요. 선택형 DLC의 소유 조회(QueryDlcOwnership)도 제공합니다.
OZeroPCStoreDetectionAction
| 값 | 설명 |
|---|---|
| Off | 로컬 Steam Anti-Piracy 응답을 적용하지 않습니다. 제한된 문제 분석 상황에서만 사용하세요. |
| Observe | 진단 정보만 기록하고 게임 실행은 계속 허용합니다. |
| Callback | OZeroSecurityManager 콜백을 발생시켜 게임이 UI 표시, 로그 기록, 자체 처리를 할 수 있게 합니다. 이 옵션만으로는 앱이 자동 종료되지 않으며, 실제 종료 여부는 Global Threat Response 설정과 프로젝트의 콜백 처리 방식에 따릅니다. |
| Block | 위반을 차단 정책으로 취급합니다. 실제 앱 종료 여부는 Global Threat Response 설정을 따릅니다. |
메서드
실행 중 Steam Anti-Piracy의 감지 후 동작을 바꿉니다. QA 빌드에서 임시로 Observe로 낮추거나, 운영자용 메뉴에서 특정 동작을 선택하게 만들 때 사용합니다.
실행 중 바꾼 Steam Anti-Piracy 동작을 해제하고 Config Dashboard 또는 Pro 포털에 설정된 기본 동작으로 되돌립니다.
예시
using OZeroSDK.Security;
// QA session: observe Steam violations without blocking gameplay.
OZeroPCStoreAntiPiracy.SetDetectionActionOverride(
OZeroPCStoreDetectionAction.Observe);
// Restore the dashboard/server policy.
OZeroPCStoreAntiPiracy.ClearDetectionActionOverride();
마지막 검증 결과
가장 최근 Steam 검증 스냅샷을 반환합니다. reported AppID, BuildID, SteamID, 서버 검증 여부, soft signal, native score 필드를 포함합니다. 디버그 UI나 QA 리포트에는 사용할 수 있지만, 게임플레이 권한 판단의 유일한 기준으로 쓰지는 마세요.
QueryDlcOwnership
현재 Steam 사용자가 선택형 DLC를 소유했는지 조회합니다. 게임을 종료하지 않는 조회 전용 API이며, DLC 콘텐츠 잠금 해제 같은 판단에 씁니다. requiredDlcAppIds에 넣은 필수 DLC는 시작할 때 자동으로 확인되므로 이 호출이 필요 없습니다. Steamworks.NET을 직접 호출하는 대신 이 API를 쓰세요.
| 필드 | 설명 |
|---|---|
| bool Owned | 현재 계정이 DLC를 소유했는지(Steam BIsSubscribedApp 기준). 잠금 해제 판단에는 이 값을 쓰세요. |
| bool Installed | DLC가 이 PC에 설치되어 있는지. 소유했지만 설치되지 않은 상태(아직 출시되지 않았거나 다운로드하지 않은 DLC)는 정상이므로 위반으로 보지 않습니다. |
| bool CrossCheckConsistent | Steam API 응답끼리 서로 맞는지 확인한 결과입니다. 응답 사이의 일관성 확인일 뿐 변조의 증거가 아니므로, 이 값만으로 차단을 판단하지 마세요. |
| string Reason | owned, not_owned, verification_unavailable(Steam이 준비되지 않아 답할 수 없음), bridge_unavailable(이 빌드에 Steam 브리지가 없음. 에디터와 Steam Anti-Piracy를 끈 빌드 포함). 오류가 나면 예외 대신 Owned=false와 이 값을 돌려줍니다. |
using OZeroSDK.Security;
// Optional DLC: unlock the content only when the account owns it.
OZeroDlcOwnership dlc = OZeroPCStoreAntiPiracy.QueryDlcOwnership(1234560);
if (dlc.Owned)
{
UnlockExpansion();
}
OZeroInstallSourceValidator OZeroSDK.Security
Android 설치 출처 검증기입니다. Install Source가 활성화되면 컴포넌트는 자동 생성됩니다. 고객 코드는 주로 지원 UI, 진단 로그, 스토어별 분기 처리를 위해 마지막 결과를 읽습니다.
메서드 및 이벤트
설치 출처 확인이 끝났을 때 호출됩니다.
가장 최근 검사 결과를 반환합니다.
필요하면 초기화를 수행한 뒤 Android 설치 출처 결과를 반환합니다.
InstallSourceResult
| DetectedSource | 해석된 설치 출처입니다. 타입은 AndroidInstallSource enum입니다. |
| RawInstallerPackage | Android PackageManager가 반환한 원본 installer package name입니다. |
| IsAuthorized | 로컬 설정과, 활성화된 경우 Pro 서버 정책이 이 설치 출처를 허용하면 true입니다. |
| ServerVerified | Pro 전용입니다. 서버 검증 호출이 완료되었으면 true입니다. |
| ServerAuthorized | Pro 전용입니다. ServerVerified가 true일 때의 서버 측 허용 여부입니다. |
AndroidInstallSource
InstallSourceResult.DetectedSource가 반환하는 정확한 enum 값입니다. manual의 스토어 이름은 읽기 쉬운 표시명이고, 코드에서는 아래 enum 이름으로 비교하세요.
| 값 | 설명 |
|---|---|
| GooglePlayStore | Google Play Store에서 설치된 경우입니다. |
| SamsungGalaxyStore | Samsung Galaxy Store에서 설치된 경우입니다. |
| AmazonAppstore | Amazon Appstore에서 설치된 경우입니다. |
| HuaweiAppGallery | Huawei AppGallery에서 설치된 경우입니다. |
| OneStore | ONE Store에서 설치된 경우입니다. |
| XiaomiGetApps | Xiaomi GetApps에서 설치된 경우입니다. |
| OppoAppMarket | OPPO App Market에서 설치된 경우입니다. |
| VivoAppStore | Vivo App Store에서 설치된 경우입니다. |
| Custom | 원본 installer package가 customAuthorizedPackages와 일치한 경우입니다. |
| ADB | Android가 빈 installer package를 반환한 경우입니다. 보통 ADB 또는 sideload 방식 설치에서 나타납니다. |
| DetectionFailed | JNI 또는 플랫폼 API를 사용할 수 없어 installer 조회 자체가 실패한 경우입니다. ADB와 다릅니다. |
| Unknown | Android가 package name을 반환했지만 기본 목록이나 커스텀 허용 목록에 없는 경우입니다. |
| Editor | Unity Editor에서 실행 중일 때 반환됩니다. |
| NotApplicable | Android 설치 출처 개념이 적용되지 않는 플랫폼에서 반환됩니다. |
OZeroDeviceBindingDetector OZeroSDK.Security
기기에 묶인 세이브 슬롯과 고객지원용 초기화 흐름을 위한 헬퍼 API입니다. Device Binding이 켜져 있으면 검증기는 자동으로 시작됩니다. 클라우드 세이브, 계정 세이브 슬롯 같은 데이터를 현재 기기와 묶고 싶을 때만 아래 토큰 메서드를 직접 호출하세요.
메서드
로컬 기기 지문을 준비한 뒤 등록하거나 검증합니다. 일반 프로젝트에서는 SDK 시작 시 자동으로 호출됩니다.
세이브 슬롯 키를 현재 기기에 연결하는 토큰을 만듭니다. 토큰은 세이브 메타데이터나 서버 기록에 저장하고, 플레이어가 직접 수정할 수 있는 세이브 본문에는 넣지 마세요.
저장된 세이브 슬롯 토큰이 현재 기기와 맞는지 확인합니다. 맞지 않으면 설정된 Device Binding 위반 응답이 발생합니다.
현재 기기 지문 해시를 반환하는 디버그/데모용 헬퍼입니다. 운영 게임 코드에서 화면에 보여주거나, 업로드하거나, 저장하지 마세요.
이 기기에 저장된 지문을 지우고 현재 기기를 다시 등록합니다. 플레이어 빌드에서 Pro 라이선스와 서버 설정이 있으면 비어 있지 않은 토큰을 백그라운드에서 서버가 발급한 Reset Token으로 검증하므로, 해제는 이 호출이 반환된 뒤에 일어납니다. 결과를 기다리려면 ClearStoredFingerprintAsync를 쓰세요. 그 밖의 경우에는 인가 콜백이 판단하며 토큰을 AuthorizationToken으로 받습니다. 서버 Reset Token을 쓰지 않는다면 RequestReset을 쓰세요.
ClearStoredFingerprint의 await 가능한 형태입니다. 메인 스레드에서 호출해 await하며, 지문을 지웠으면 true를 돌려줍니다. 서버 검사가 메인 스레드를 막지 않으므로 HTTP가 메인 스레드에서 도는 환경(Windows·macOS IL2CPP, WebGL)에서도 게임이 멈추지 않습니다.
저장된 지문을 지우도록 요청합니다. 플레이어 빌드에서는 인가 콜백이 판단하며, reason은 로깅용으로 콜백에 전달됩니다. 승인되면 지문을 지우고 같은 세션에서 현재 기기를 다시 등록한 뒤 true를 돌려줍니다. 에디터에서는 항상 해제됩니다.
플레이어 빌드에서 해제를 승인(true)하거나 거부하는 게임의 콜백을 등록합니다. null을 넘기면 등록이 해제됩니다. 콜백은 해제를 요청한 스레드에서 동기로 호출됩니다. 인가 콜백이 없으면 플레이어 빌드는 유효한 서버 Reset Token이 없는 한 해제를 거부합니다.
true를 돌려주세요. 항상 true를 돌려주는 콜백은 프로세스 안의 어떤 코드든 바인딩을 지울 수 있게 만듭니다. 함께 제공되는 데모 씬은 시연을 위해 모든 요청을 승인합니다.
OZeroDeviceBindingResetRequest
인가 콜백에 전달되는 읽기 전용 구조체입니다.
RequestReset에 넘긴 사유입니다. 없으면 빈 문자열입니다.
ClearStoredFingerprint에 넘긴 토큰입니다. 없으면 빈 문자열입니다.
예시
using OZeroSDK.Security;
var detector = OZeroDeviceBindingDetector.Instance;
string slotKey = "account:1234:slot:main";
string token = detector.BindToSaveSlot(slotKey);
// Store token next to your save metadata.
bool ok = detector.ValidateSaveSlot(slotKey, token);
// Reset flow: register an authorizer that performs your own check.
OZeroDeviceBindingDetector.SetResetAuthorizer(request =>
MyAccountService.HasVerifiedResetFor(request.Reason));
bool cleared = detector.RequestReset("ticket-1234");
OZeroSecurityConfig ScriptableObject
OZero 모듈의 보안 설정입니다. 에디터에서는 Edit > Project Settings > OZero Security에서 편집하며, ProjectSettings/OZeroSecuritySettings.asset(공유)와 UserSettings/OZeroSecuritySecrets.json(사용자별 비밀값)에 저장되고 빌드 때 플레이어에 구워집니다. 코드에서 현재 설정은 OZeroSecurityConfig.Instance로 읽고, 에디터 스크립트에서 바꾸려면 OZeroSecurityProjectSettings(Load, LoadOrCreate, Save)를 사용합니다.
필드
필드는 Response, Integrity 같은 중첩 설정 클래스로 그룹화됩니다. 아래 표는 코드에서 자주 확인하거나 연동 중 자주 조정하는 필드를 정리한 요약이며, 전체 Inspector 설정 표는 manual을 참고하세요. 기본값은 코드에 저장된 직렬화 기본값 기준입니다. 릴리스 빌드에서 런타임 값이 강제로 달라지는 항목은 *로 표시합니다.
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| — 최상위 — | |||
| developerSecret | string | "" | OZeroSV_File과 OZeroSafePlayerPrefs 데이터를 보호하는 프로젝트별 secret입니다. 첫 릴리스 전에 Config Dashboard의 Generate Secure Secret으로 생성하고, 출시 후에는 변경하지 마세요. 변경하면 기존 보호 데이터를 새 버전에서 복호화할 수 없습니다. |
| enableLog | bool | true | SDK 디버그 로그를 켭니다. 개발/QA 원인 분석에는 유용하지만, 릴리스 빌드에서는 로그 노출 정책을 별도로 확인하세요. |
| enableFailureDiagnostics | bool | false | 보안 실패 진단 파일을 Application.persistentDataPath에 저장합니다. QA 또는 고객 지원 중에만 켜고, 확인 후 다시 끄세요. |
| — Response — | |||
| response.forceQuitOnDetection | bool | true | 위협이 확인되었을 때 앱을 자동 종료할지 결정합니다. QA 중에는 끄고 이벤트만 관찰할 수 있지만, 출시 빌드에서는 프로젝트 정책에 맞게 명확히 선택하세요. |
| response.fatalCallbackGraceSeconds | float | 10 | 위협 감지 후 게임 쪽 보안 콜백 UI가 플레이어에게 안내할 수 있는 최대 시간(초)입니다. 즉시 종료를 원할 때만 0으로 설정하세요. |
| securityTerminationNoticePolicy | OZeroSecurityTerminationNoticePolicy | Disabled | 이미 대기 중인 치명적 보안 종료에 적용하는 전체 라이선스 공통 선택적 알림 정책입니다. |
| securityUiDialogPrefabResourcePath | string | "" | 공유 보안 UI 프리팹의 선택적 Resources 경로입니다. 비워 두면 SDK 기본값을 사용합니다. |
| securityUiLanguageCode | string | auto | 공유 보안 UI 문자열의 언어 코드입니다. auto는 Application.systemLanguage를 따릅니다. |
| securityUiFallbackLanguageCode | string | en | 선택한 공유 현지화 JSON이 없을 때 사용할 대체 언어입니다. |
| — Managed Verification UI — | |||
| Security Config > Pro - License & Online Services > Online Verification UI > Ui Policy (Pro) | OZeroManagedVerificationUiPolicy | BuiltInBlockingDialog | OZero Managed Build Integrity와 Steam DRM 상태를 어떤 사용자 UI 흐름으로 보여 줄지 선택합니다. 기본 모드는 SDK가 nonce/attest, managed session verification, 온라인 필요 안내, 재시도, timeout UI를 처리합니다. SDK 다이얼로그를 대체할 때만 Custom UI / Callback Only를 사용하세요. |
| Security Config > Pro - License & Online Services > Online Verification UI > Retry Timeout Seconds (Pro) | int | 15 | 플레이어가 Retry를 누른 뒤 retry-timeout으로 보고하기 전까지 기다리는 최대 시간입니다. |
| Security Config > Pro - License & Online Services > Online Verification UI > Online Required Timeout Seconds (Pro) | int | 120 | 온라인 필요 상태를 유지하다가 설정된 timeout action을 적용하기 전까지 기다리는 최대 시간입니다. |
| Security Config > Pro - License & Online Services > Online Verification UI > Timeout Action (Pro) | OZeroManagedVerificationTimeoutAction | BlockSession | timeout 시 다이얼로그 유지, 보호 세션 차단, 앱 종료, 콜백만 호출 중 어떤 동작을 수행할지 선택합니다. |
| Security Config > Pro - License & Online Services > Online Verification UI > Auto Retry When Network Restored (Pro) | bool | false | 네트워크 연결이 복구되면 managed verification을 자동으로 다시 시도합니다. 명시적인 플레이어 조작 없이 재시도해도 안전한 게임 흐름에서만 활성화하세요. |
| — Integrity — | |||
| integrity.useIntegrity | bool | true | Build Integrity 모듈을 켜는 마스터 스위치입니다. |
| integrity.validateOnStartup | bool | true | Start() 시점에 전체 무결성 검사를 실행합니다. |
| integrity.periodicCheckInterval | float | 300 | 주기적 재검증 실행 간격(초)입니다. 코드 기본값은 300이며, 0 이하로 설정하면 주기 검사를 비활성화합니다. |
| integrity.checkAssemblyHash | bool | true | OZeroAssemblyManifest를 기준으로 컴파일된 어셈블리의 SHA-256 / 공개 키 토큰 검증. |
| integrity.checkDebugger | bool | true | 연결된 매니지드 디버거, Unity 디버그 빌드 플래그, CPU 타이밍 이상 감지. |
| integrity.checkPlatformNative | bool | true | 플랫폼별 네이티브 검사(루팅, 탈옥, APK 서명, Windows 코드 서명 등) 실행. |
| integrity.failIfManifestMissing | bool | false* | manifest 누락/로드 실패를 위반으로 처리합니다. *development가 아닌 플레이어 빌드에서는 직렬화 값과 무관하게 true로 강제됩니다. |
| integrity.failIfAssemblyHashBlobMissing | bool | false* | 생성된 어셈블리 해시 블롭 누락을 위반으로 처리합니다. *development가 아닌 플레이어 빌드에서는 true로 강제됩니다. |
| integrity.requireManifestSignature | bool | false* | manifest에 유효한 서명을 요구합니다. 키는 Window → OZero Security → Config & Dashboard의 Generate Key Pair로 생성하세요. *릴리스 플레이어 빌드에서는 true로 강제됩니다. |
| integrity.il2cppHashGlobalGameManagers | bool | false | IL2CPP 데스크톱(Windows·macOS·Linux) 파일 해시에 globalgamemanagers를 포함합니다. Standard와 Strict 프리셋은 이 값을 켭니다. |
| integrity.il2cppHashSharedAssets | bool | false | IL2CPP 데스크톱 파일 해시에 sharedassets* 파일을 포함합니다. Standard와 Strict 프리셋은 이 값을 켭니다. |
| integrity.il2cppHashSceneFiles | bool | false | IL2CPP 데스크톱 파일 해시에 level* 같은 Unity 씬 파일을 포함합니다. Standard와 Strict 프리셋은 이 값을 켭니다. |
| integrity.blockEmulator | bool | true | (Android) 에뮬레이터 감지를 무결성 위반으로 처리. |
| Security Config > Build Integrity > Pro - Online > Check Integrity With Server (Pro) | bool | false | Pro의 nonce → attest 흐름을 켭니다. OZero Managed와 기본 Managed Verification UI를 함께 쓰면 SDK가 빌드 무결성 증거 제출, managed session 판정 요청, 사용자 재시도/timeout 상태 표시를 게임 코드 없이 처리합니다. |
| Security Config > Build Integrity > Pro - Online > Attestation Verification Mode (Pro) | enum | OZeroManaged | 최종 검증을 맡을 주체를 선택합니다. 기본값과 Low·Standard 보안 프리셋은 OZeroManaged로, SDK가 OZero 서버에 검증을 요청합니다. Strict 프리셋은 CustomerGameServer로, 게임 서버가 발급한 정보로 클라이언트에서 RequestGameServerAttestation(...)을 호출한 뒤 게임 서버가 OZA 토큰을 검증합니다. 서버 검증을 사용하려면 checkIntegrityWithServer를 별도로 켜야 합니다. 라이선스 등급이 Pro가 아니면 어떤 프리셋을 적용해도 OZeroManaged로 유지됩니다. |
| Security Config > Build Integrity > Pro - Online > Attestation Network Policy (Pro) | enum | RequireOnlineRevalidation | 지원하는 정책은 RequireOnlineRevalidation 하나입니다. 서버 검증을 완료하거나 갱신할 수 없으면 OnlineRequired 상태로 전환하여 온라인 재검증을 요구합니다. 검증되지 않은 오프라인 세션을 허용하는 옵션은 없습니다. |
| — InstallSource (Android) — | |||
| installSource.useInstallSource | bool | true | 설치 출처 검증 마스터 스위치. |
| installSource.allowGooglePlayStore | bool | true | Google Play 설치 허용(Galaxy Store, Amazon Appstore, AppGallery, OneStore 등 개별 스토어 플래그도 토글 가능). |
| Security Config > Install Source (Android Only) > Pro - Online > Check Installer With Server (Pro) | bool | false | 로컬 감지 후 /v1/install-source/verify를 호출해 Pro 서버 allowlist와 감사 로그를 적용합니다. |
| installSource.allowDetectionFailed | bool | false | Android installer 조회 자체가 실패해도 시작을 허용합니다. 검증된 기기별 사유가 없다면 릴리스에서는 꺼두세요. |
| installSource.allowUnknownSources | bool | false | 기본 목록과 customAuthorizedPackages에 없는 installer package를 허용합니다. |
| — Steam Anti-Piracy — | |||
| steamAntiPiracy.useSteamAntiPiracy | bool | false | Steam 실행, 권한, 필수 DLC 소유, 릴리스 hygiene 검사의 master switch입니다. |
| steamAntiPiracy.detectionAction | OZeroPCStoreDetectionAction | Callback | Steam 검증 실패 시 적용할 로컬 응답입니다. Observe는 기록만 하고 콜백을 올리지 않습니다. Callback은 보안 콜백을 올리며, reportViolationToCallback이 꺼져 있으면 Observe로 낮아집니다. Block은 이 설정과 무관하게 콜백을 강제로 올립니다. 두 경우 모두 앱 종료 여부는 forceQuitOnDetection이 정합니다. 즉 Block만으로 앱이 종료되지는 않습니다. Pro 정책이 이 값을 override할 수 있습니다. |
| Security Config > Steam Anti-Piracy (PC Only) > Pro - Online > Check Steam DRM With Server (Pro) | bool | false | 서버 근거를 사용한 Steam DRM 검증을 켭니다. OZero Managed와 기본 Managed Verification UI를 함께 쓰면 SDK가 Steam ticket 제출, activation token cache, 온라인 필요 UI, 재시도, timeout 상태를 처리합니다. |
| Security Config > Steam Anti-Piracy (PC Only) > Pro - Online > Steam Drm Verification Mode (Pro) | enum | OZero Managed | OZero 직접 검증 또는 고객 게임 서버 검증 중 어떤 방식으로 Steam DRM 검증을 운영할지 선택합니다. OZero Managed는 기본 Managed Verification UI와 함께 사용할 때 자체 백엔드 없는 통합 경로입니다. |
| Security Config > Steam Anti-Piracy (PC Only) > Pro - Online > Steam Drm Network Policy (Pro) | enum | RequireOnlineRevalidation | Steam DRM 재검증이 필요한 시점에 서버에 연결할 수 없으면 온라인 필요 상태로 처리합니다. 기본 Managed Verification UI 모드는 재시도 흐름을 자동으로 표시합니다. |
| — DeviceBinding — | |||
| deviceBinding.useDeviceBinding | bool | true | SDK 시작 시 Device Binding 검증을 켭니다. |
| deviceBinding.hardwareChangeTolerance | int (0–3) | 1 | 기기 지문을 구성하는 항목이 몇 개까지 달라져도 같은 기기로 볼지 정합니다. |
| Security Config > Device Binding > Pro - Online > Verify Device With Server (Pro) | bool | false | Pro 전용입니다. /v1/device/register와 /v1/device/verify로 기기 지문을 등록하고 검증합니다. 네트워크 실패는 게임을 막지 않지만, 서버의 명확한 거부는 Device Binding 위반이 됩니다. |
| — SpeedHack — | |||
| speedHack.useSpeedHack | bool | true | 스피드핵 디텍터 마스터 스위치. |
| speedHack.checkInterval | float | 1.0 | 검사 주기(초)입니다. 너무 작거나 큰 값은 안전 범위 안에서 자동 보정됩니다. |
| speedHack.requiredDetections | int | 3 | 위반으로 판단하기 전에 필요한 연속 의심 횟수입니다. 너무 작거나 큰 값은 안전 범위 안에서 자동 보정됩니다. |
| speedHack.detectSlowHack | bool | false | 느린 시간 조작도 감지합니다. 의도적인 슬로우 모션 오탐을 줄이기 위해 기본값은 꺼져 있습니다. |
| speedHack.useWebTimeValidation | bool | true | 외부 엔드포인트와의 HTTPS HEAD 기반 게임 시간 교차 검증을 활성화합니다. |
| speedHack.webTimeUrls[] | string[] | [] | 웹 시간 교차 검증에 사용할 HTTPS 주소 목록입니다. 직접 운영하거나 신뢰할 수 있는 주소를 2개 이상 설정하세요. 목록이 비어 있으면 웹 시간 검증에 사용할 주소가 없습니다. |
| speedHack.minSuccessfulEndpoints | int | 2 | 한 번의 web-time 검사 라운드를 성공으로 인정하려면, webTimeUrls 중 최소 몇 개가 유효한 Date 헤더를 돌려줘야 하는지 정합니다. |
| speedHack.maxConsecutiveFailures | int | 6 | web-time 검사 라운드가 이 횟수만큼 연속 실패하면 onWebTimeUnavailable 정책을 실행합니다. |
| speedHack.onWebTimeUnavailable | WebTimeUnavailablePolicy | WarnOnly | 웹/서버 시간을 계속 확인하지 못할 때의 정책입니다. WarnOnly는 로그만 남기고 계속 실행합니다. Strict는 반복 실패 후 SpeedHack 이벤트를 발생시킵니다. Silent는 로그도 남기지 않으므로 특수 테스트용으로만 두세요. WebGL에서는 브라우저 보안 정책 때문에 다른 도메인의 Date 헤더를 읽을 수 없어 웹 시간 확인이 항상 실패하므로, Strict도 WarnOnly로 적용됩니다. |
| speedHack.enableRemoteSpeedHackConfig | bool | true | Pro에서 서버의 Speed & Time Hack 설정을 받아 적용합니다. 새 설정의 기본값은 true이며, 보안 프리셋은 라이선스 등급이 Pro일 때만 켜고 Standard에서는 끕니다. 포털에서 값을 지정한 항목만 로컬 값을 덮으며, 임계값뿐 아니라 detectSlowHack·enableTimeScaleDetection·enableThreadTimerCheck·enableSignedServerTime 같은 탐지 켜기/끄기도 포함됩니다. 포털에서 비워 둔 항목은 로컬 값을 유지합니다. 끄거나 서버에 연결할 수 없으면 로컬 설정만 사용합니다. |
| speedHack.remoteSpeedHackConfigInterval | float | 300 | /v1/speedhack-config를 다시 가져오는 주기입니다. 0이면 앱 시작 시 한 번만 가져옵니다. |
| speedHack.remoteSpeedHackConfigJitterPercent | float | 20 | Pro 원격 설정 요청 시간이 여러 기기에서 겹치지 않도록, 설정한 갱신 주기 주변에서 호출 시점을 분산하는 비율입니다. 0~75 범위로 제한됩니다. |
| speedHack.enableSignedServerTime | bool | false | Pro 서버 기능입니다. Pro 활성화가 가능할 때 서명된 /v1/time을 우선 신뢰 시간으로 사용합니다. 여러 번 실패하면 설정된 web-time endpoint로 되돌아갑니다. |
| — PhysicsHack — | |||
| physicsHack.useGlobalPhysicsHack | bool | true | 모든 OZeroPhysicsHackDetector 컴포넌트를 한 번에 켜거나 끄는 전역 스위치입니다. 개별 이동 기준값은 각 컴포넌트 Inspector에 남아 있습니다. |
| Security Config > Pro - License & Online Services > Telemetry > Enable Remote Physics Config (Pro) | bool | true | Pro에서 앱 시작 시 전역 PhysicsHack 설정을 받아 적용합니다. 새 설정의 기본값은 true이며, 보안 프리셋은 라이선스 등급이 Pro일 때만 켜고 Standard에서는 끕니다. 포털에서 지정한 항목(useGlobalPhysicsHack, telemetryThrottlePerMinute)만 로컬 값을 덮고, 텔레메트리는 서버가 끌 수만 있고 켤 수는 없습니다. 끄거나 서버에 연결할 수 없으면 로컬 설정을 사용합니다. 캐릭터별 이동 임계값은 별도 설정입니다. 코드에서는 읽기 전용 속성 EnableRemotePhysicsConfig로 확인할 수 있습니다. |
| Security Config > Pro - License & Online Services > Telemetry > Enable Server Telemetry (Pro) | bool | true | Pro 전용이며 기본값은 켜짐입니다. 활성 라이선스에 권한이 있는 한 프로젝트가 끄지 않으면 일반 보안 이벤트와 상세 PhysicsHack telemetry를 전송합니다. 끄면 신규 telemetry 전송을 중단하며 서버 정책은 로컬에서 꺼진 전송을 켤 수 없습니다. |
| Security Config > Pro - License & Online Services > Telemetry > Telemetry Throttle Per Minute (Pro) | int | 30 | 이 클라이언트가 1분 동안 보낼 수 있는 PhysicsHack telemetry 수입니다. 0은 제한 없음이라, 잘못 설정된 detector가 서버를 과도하게 호출할 수 있어 권장하지 않습니다. |
| — Injection — | |||
| injection.useInjection | bool | true | Injection & Hooking 전체 스위치입니다. 플레이어 빌드에서는 설정된 대응 정책에 따라 처리하고, 개발 빌드에서는 원인 확인을 위한 경고 중심 진단 흐름으로 동작합니다. 에디터 Play Mode에서는 Editor Play Mode Protection이 Test일 때만 검사합니다. |
| Security Config > Injection & Hooking > Pro - Online > Enable Remote Injection Config (Pro) | bool | true | Pro에서 서버의 Injection 설정을 받아 적용합니다. 새 설정의 기본값은 true이며, 보안 프리셋은 라이선스 등급이 Pro일 때만 켜고 Standard에서는 끕니다. 서버에서 지정한 항목만 덮어쓰고, 끄면 로컬 설정만 사용합니다. 서버 화이트리스트 수신은 enableServerWhitelist로 별도 설정합니다. |
| Security Config > Injection & Hooking > Pro - Online > Enable Server Whitelist (Pro) | bool | false | Pro에서 서명된 모듈 허용·차단 목록을 받습니다. enableServerWhitelist 하나로 수신을 제어하며, 승인 목록과 차단 목록의 유효기간은 각각 관리됩니다. 허용된 모듈은 무음 처리하고 명시적으로 차단한 모듈에는 차단 정책을 적용합니다. 검증된 목록만 캐시에 저장하며, 오프라인에서는 유효기간이 남은 결정만 사용합니다. 기본값은 false입니다. |
| Security Config > Injection & Hooking > Pro - Online > Enable Injection Scoped Exact (Pro) | bool | false | Pro에서 관측된 모듈을 전체 목록을 받는 대신 해시로 하나씩 서버에 조회합니다. 아직 제공되지 않습니다: OZero 서버에서 이 조회 기능이 켜져 있지 않아, 켜도 효과가 없고 SDK는 기존 서버 화이트리스트를 그대로 사용합니다. 꺼 두세요. |
| Security Config > Injection & Hooking > Pro - Online > Enable Injection Bloom Advisory (Pro) | bool | false | 서명된 전역 모듈 인덱스로 조회 우선순위를 정합니다(참고용). 아직 제공되지 않습니다: 전역 인덱스 루트 키가 공개되지 않아, enableInjectionScopedExact와 함께 켜면 root_unconfigured로 빌드가 실패합니다. 꺼 두세요. |
| injection.serverWhitelistRefreshInterval | float | 0 s | 실행 중 서버 목록을 정기적으로 다시 받는 간격입니다. 0이면 정기 조회만 중지하며, 시작 시 수신과 목록 만료 전 자동 갱신은 계속 수행합니다. |
| injection.enableWindowsSystemModuleScan | bool | true | Windows의 System32, SysWOW64, WinSxS에서 게임에 로드된 모듈을 검사합니다. 켜면 시스템 폴더에 있다는 이유만으로 모듈이나 후킹 대상을 신뢰하지 않습니다. Low: false, Standard/Strict: true. 모듈 식별에는 enableWindowsModuleIdentityScan도 켜져 있어야 합니다. 인라인 후킹 검사는 별도 옵션으로 설정합니다. |
| injection.injectionWhitelistEntries | OZeroInjectionWhitelistEntry[] | empty | Injection Detector에 등록하는 로컬 신뢰 모듈 목록입니다. Pro 전용이 아니라 모든 티어에서 사용할 수 있습니다. 게임과 함께 배포하거나 QA, 진단 파일, Pro telemetry, OZero 지원팀 안내로 검증한 모듈만 추가하세요. |
WebTimeUnavailablePolicy
설정된 web-time 주소가 maxConsecutiveFailures 라운드만큼 연속 실패한 뒤 적용되는 정책입니다. 일시적인 네트워크 문제를 로그로만 볼지, 보안 콜백으로 올릴지 정합니다.
| 값 | 설명 |
|---|---|
| WarnOnly | 기본값입니다. 경고 로그만 남기고 게임은 계속 실행합니다. 오프라인에서도 실행되어야 하는 게임에는 이 선택이 가장 안전합니다. |
| Strict | 반복 실패 후 SpeedHack 콜백을 발생시킵니다. 네트워크 품질이 낮아도 web-time 실패가 발생할 수 있으므로, 실제 서비스 지역의 네트워크에서 테스트한 뒤 사용하세요. |
| Silent | 로그도 남기지 않고 콜백도 발생시키지 않습니다. 짧은 호환성 테스트용으로만 두고, 릴리스 빌드에는 권장하지 않습니다. |
OZeroLicenseConfig OZeroSDK.Security.License
온라인 라이선스 설정입니다. 에디터에서는 ProjectSettings/OZeroLicenseSettings.asset에 저장되고 Window > OZero Security > Online Services 창이나 Edit > Project Settings > OZero Security의 Pro 카드에서 편집하며, 빌드 때 암호화 리소스로 플레이어에 들어갑니다. 라이선스 티어를 선택하고 Pro 라이선스 키와 Pro 런타임 서버 설정을 보관합니다. 설정 파일이 없거나 키가 비어 있으면 Standard와 같은 방식으로 동작합니다. 에디터 스크립트에서 바꾸려면 OZeroSDK.Security.License.OZeroLicenseProjectSettings(Load, LoadOrCreate, Save, Reload)를 사용합니다.
필드
| 필드 | 타입 | 설명 |
|---|---|---|
| tier | OZeroLicenseTier | Standard는 완전 오프라인으로 동작합니다. Pro는 프로젝트 바인딩 native variant와 서버 기반 런타임 기능을 활성화합니다. |
| licenseKey | string | 프로젝트에 발급된 Pro 라이선스 키입니다. Pro는 프로젝트 전용 Native Variant 확인, 런타임 활성화, 서버 기능에 사용합니다. 비어 있으면 Standard와 같은 방식으로 동작합니다. |
| serverBaseUrl | string | Pro 런타임 서버 Base URL입니다. 활성화, 텔레메트리, signed time, attestation, 서버 정책 호출에 사용됩니다. Standard 런타임은 이 URL을 호출하지 않습니다. |
| serverPublicKeyHex | string | Pro 전용 서버 서명 공개키입니다. 고객 포털 > Server Key의 Active publicKey를 복사해 넣으며, Pro 런타임의 signed activation, time, attestation, offline policy 토큰 검증에 사용합니다. Pro Variant manifest는 이 필드가 아니라 SDK에 내장된 OZero Variant signing key로 검증됩니다. |
| previousServerPublicKeyHex | string | Pro 서버 키 교체 유예 기간에만 사용하는 이전 서명용 공개키입니다. 평소에는 비워 둡니다. |
| offlineProPolicyMode | OZeroOfflineProPolicyMode | 디바이스가 오프라인일 때 로컬 Build Integrity가 서명된 Pro 포털 차단 정책을 어떻게 적용할지 정합니다. |
| activationTimeoutSeconds | float | SDK가 Pro 활성화 응답을 기다리는 시간입니다. 포털 정책이 허용하면 저장된 Pro 정보를 사용합니다. 새 온라인 확인이 필요한 경우에는 서버 응답 없이 앱 시작을 계속할 수 없습니다. |
| enableLog | bool | 더 이상 인스펙터에 표시되지 않는 이전 필드입니다. 라이선스 흐름 로그는 OZeroSecurityConfig의 Enable Debug Logs를 따릅니다. |
| enableDevicePolicyHeartbeat | bool | Pro 전용. 현재 기기가 계속 허용 상태인지 주기적으로 확인합니다. |
| devicePolicyHeartbeatInterval | float | Pro 기기 정책 확인 기본 주기입니다. 기본값은 300초이며, 0이면 주기 확인을 끕니다. |
| devicePolicyHeartbeatJitterPercent | float | 여러 기기가 같은 순간에 기기 정책을 확인하지 않도록, 확인 시간을 설정 주기 주변으로 조금씩 분산하는 비율입니다. 0~75 범위로 제한됩니다. |
| enableSecurityLevelCheck | bool | Pro 전용. 빌드가 기대한 보안 레벨을 선언했는지 서버가 확인할 수 있게 합니다. |
| declaredSecurityLevel | OZeroDeclaredSecurityLevel | 이 빌드가 서버에 선언하는 보안 레벨입니다. |
| failOnSecurityLevelReject | bool | true이면 서버가 보안 레벨 또는 설정 해시를 명시적으로 거부했을 때 설정된 강한 대응을 실행합니다. |
| securityLevelCheckInterval | float | 서버 보안 레벨 재확인 주기입니다. 0이면 앱 시작 시 한 번만 확인합니다. |
| securityLevelCheckJitterPercent | float | 여러 기기가 같은 순간에 보안 레벨을 다시 확인하지 않도록, 확인 시간을 설정 주기 주변으로 조금씩 분산하는 비율입니다. 0~75 범위로 제한됩니다. |
로컬 시계 정책
이 정책은 고객 포털 > 라이선스 > 로컬 시계에서 관리하므로 Unity에서 맞춰야 할 설정값은 없습니다. 온라인 Pro 활성화에는 서버 시간이 사용됩니다. 허용 오차는 저장된 Pro 활성화 정보를 오프라인 시작에 사용할 수 있는지 판단할 때만 적용됩니다.
OZeroDeclaredSecurityLevel
보안 레벨 검증을 켰을 때 Pro 서버로 보내는 enum입니다. 서버는 이 값이 라이선스에 설정된 최소 보안 레벨을 만족하는지 확인합니다.
| 값 | 설명 |
|---|---|
| Low | 프로토타입 또는 개발 빌드 수준입니다. 서버 정책에서 낮은 보호 선언을 허용할 때만 사용하세요. |
| Standard | 기본값이며 일반적인 보호 빌드에서 권장하는 라이브 게임용 선언입니다. |
| Strict | 가장 강한 보호 선언입니다. 프로젝트가 strict 정책으로 정상 동작하는지 QA로 확인한 뒤 사용하세요. |
OZeroOfflineProPolicyMode
디바이스가 서버에 연결할 수 없을 때 로컬 Build Integrity가 서명된 Pro 포털 차단 정책을 어떻게 처리할지 정하는 enum입니다. 값은 fail-open 운영, fail-closed 정책 강제, 테스트 bypass 동작을 명확히 나눕니다.
| 값 | 설명 |
|---|---|
| ApplyCachedBlockPolicies | 권장 fail-open 기본값입니다. 오프라인 중 유효한 서명 정책 캐시가 있으면 적용하고, 사용할 수 있는 캐시가 없으면 이 정책 게이트는 통과합니다. |
| RequireFreshPolicy | 엄격한 fail-closed 모드입니다. 사용할 수 있는 서명 정책이 없으면 서버 Integrity가 켜져 있을 때 Build Integrity를 실패시킵니다. |
| IgnoreCachedBlockPolicies | Bypass 모드입니다. 오프라인 중 캐시된 Pro 차단 정책을 읽지 않습니다. 테스트나 마이그레이션 외에는 라이브 빌드에 사용하지 마세요. |
프로퍼티
Resources에서 런타임 설정을 로드합니다. null이면 Standard와 같은 방식으로 처리하세요.
Standard 또는 빈 라이선스 키이면 true입니다. Pro 활성화가 필요한 경우 false입니다.
Pro에서 true입니다. Native Variant 검증 대상 등급을 식별하며, private Variant 존재 여부는 가져온 서명 manifest에서 자동 판별합니다.
현재 프로젝트에서 명시적인 native variant 호환성 옵션이 켜져 있을 때만 true입니다.
OZeroLicenseRuntime OZeroSDK.Security.License
현재 라이선스 상태를 읽는 실행 중 API입니다. 앱 시작 시 자동 초기화되므로 대부분의 프로젝트는 상태를 읽거나 HasCapability만 호출하면 됩니다.
프로퍼티
| 이름 | 타입 | 설명 |
|---|---|---|
| Entitlement | OZeroLicenseEntitlement | 현재 활성화된 Pro 권한 정보입니다. Standard 모드에서는 null입니다. |
| HasEntitlement | bool | 현재 Pro 활성화 정보가 있으면 true입니다. |
| IsServerless | bool | SDK가 Pro 서버 기능 없이 실행 중이면 true입니다. |
| Initialized | bool | 라이선스 런타임의 첫 시작 처리가 끝나면 true입니다. |
| IsProDowngraded | bool | SDK가 명시적으로 Standard 동작으로 전환된 경우 true입니다. 온라인 확인을 요구하는 포털 정책이 적용된 경우에는 이 전환을 사용하지 않고 앱 시작을 중단합니다. |
| DowngradeReason | string | 가장 최근 자동 다운그레이드(기본 보호 전환)의 진단 사유입니다. |
| CurrentDeviceId | string | 활성화와 기기 정책에 쓰는 기기 ID입니다. 기본 설정에서는 Customer Portal과 텔레메트리에 보이는 값과 같습니다. 필요하면 게임 서버에 저장하세요. |
| HasExternalUserId | bool | SetExternalUserId로 사용자 UUID가 연결되어 있는 동안 true입니다. |
| LastServerBlockCode | string | 세션을 끝낸 마지막 서버 차단 코드입니다. "USER_BLOCKED"(사용자 UUID 차단) 또는 "DEVICE_BLOCKED"(기기 차단)이며, 없으면 null입니다. 보안 콜백에서 읽어 둘을 구분하세요. |
| DeviceIdProvider | Func<string> | 활성화에 쓰는 기기 ID를 바꿉니다. 바꾸는 것은 권장하지 않습니다. 서버가 기기를 다른 ID로 알게 되고, 텔레메트리는 원래 ID를 그대로 씁니다. 게임 계정 ID를 연결하려면 기기 ID를 유지하는 SetExternalUserId를 쓰세요. |
메서드
여러 번 호출해도 안전한 시작 메서드입니다. 보통 SDK가 자동 호출하며, 커스텀 부트스트랩에서는 라이선스 상태를 읽기 전에 await할 수 있습니다.
현재 활성화 정보에 telemetry, signed_time, attestation 같은 기능 권한이 있는지 반환합니다. Standard에서는 false입니다.
[Pro] 사용자 UUID(계정 ID)를 이 세션에 연결합니다. 로그인 뒤 호출하세요. 값은 기기 안에서 해시되고 해시만 전송됩니다. 앞뒤 공백을 지운 뒤 비어 있거나 128자를 넘거나 제어 문자가 있으면 false를 돌려줍니다. 활성화 전에 호출해도 되며 해시는 활성화 뒤에 전송됩니다. 약 5초 뒤 기기 정책 확인이 실행됩니다(최소 30초 간격).
[Pro] 사용자 UUID 연결을 해제합니다. 로그아웃할 때 호출하세요.
SHA-256("OZERO-EXTUID-V1|" + salt + "|" + uuid)의 소문자 hex입니다. salt는 라이선스의 64자 hex salt, uuid는 앞뒤 ASCII 공백을 지운 값(대소문자 유지)입니다. 테스트 벡터: salt 000102…1e1f, player-123 → 2d5a23607064e6dbc02dc32dc96af8cf54862d2f51d778596cfbe9d6ba8f8a3a.
라이선스 서버 런타임 호출
Pro 기능은 /v1 아래의 HTTPS JSON API를 사용합니다. OZA v2는 audience, 256-bit challenge, session ID와 evidence에 결합된 1회성 토큰입니다. 자체 게임 서버의 /v1/validate 호출은 License Server API Key와 동일한 binding 값을 반드시 제출하며, 성공한 토큰은 항상 원자적으로 consume됩니다. 자체 서버가 없는 팀은 root-signed keyset과 OZMS receipt를 검증하는 OZero Managed 흐름을 사용합니다.
| 엔드포인트 | 용도 |
|---|---|
| POST /v1/activate | 현재 기기에서 Pro 라이선스를 활성화하고 로컬 활성화 정보를 갱신합니다. |
| GET /v1/time | 활성화된 경우 Speed & Time Hack 검증에 사용할 signed server time을 제공합니다. |
| POST /v1/attest | 활성화된 무결성 검사 통과 후 고유 토큰 ID가 포함된 Pro build attestation 토큰을 발급합니다. nonce는 제출된 빌드 증거와 앱 식별 정보에 묶입니다. |
| POST /v1/validate | 게임 서버에서 OZA v2를 검증합니다. Server API Key와 audience/challenge/sessionId가 필수이며, 성공한 token은 항상 한 번만 consume됩니다. |
| POST /v1/managed-session | 자체 백엔드가 없는 팀을 위해 OZero가 Pro OZA 토큰을 검증하고 허용/경고/차단 결과와 짧은 세션을 반환합니다. SDK는 세션 만료 전에 자동 재검증을 시도하며, 같은 토큰을 재사용하는 흐름은 차단됩니다. |
| POST /v1/telemetry | Pro 텔레메트리 권한이 활성화된 경우 보안 이벤트를 서버로 전송합니다. |
| /v1/customer-api/* | 고객 서버 API: read / block 범위가 있는 서버 API 키로 사용자·기기를 조회하고 사용자 차단 규칙을 관리합니다. 고객 서버 API를 참고하세요. |
POST /v1/activate 계약
Unity SDK가 Pro 라이선스를 활성화할 때 보내는 기본 요청 계약입니다. 서버 스키마는 네이티브 검증용 필드를 추가로 받을 수 있지만, 현재 SDK의 기본 활성화 요청은 아래 필드를 보냅니다.
| 필드 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
| licenseKey | string | yes | /v1/activate에 사용하는 Pro 라이선스 키입니다. 서버는 키 형식, 상태, 티어, 만료를 확인합니다(활성화와 기기 수는 무제한). |
| deviceId | string | yes | OZeroLicenseRuntime.DeviceIdProvider에서 가져온 기기 식별자입니다. 캐시 바인딩과 기기 정책에 사용되며, 기기 수를 세는 데는 쓰지 않습니다. |
| sdkVersion | string | yes | SDK 버전 문자열입니다. 서버는 semver에 가까운 형식을 검증하고 활성화 기록에 저장합니다. |
| platform | enum string | yes | windows, windows_server, macos, linux, linux_server, ios, android, webgl, unknown 중 하나입니다. |
| appIdentifier | string | optional | 가능한 경우 Unity Application.identifier 값입니다. 라이선스 identity 정책과 비교됩니다. |
| companyName | string | optional | 가능한 경우 Unity Application.companyName 값입니다. |
| productName | string | optional | 가능한 경우 Unity Application.productName 값입니다. |
| webglOrigin | string | optional | WebGL 빌드에서 Unity Application.absoluteURL로 확인한 HTTP(S) origin입니다. |
| 필드 | 타입 | 설명 |
|---|---|---|
| activated | bool | 활성화가 승인되면 true입니다. |
| tier | string | 서버가 결정한 라이선스 티어입니다. |
| capabilities | string[] | OZeroLicenseRuntime.HasCapability에서 사용하는 기능 목록입니다. |
| serverFeaturesEnabled | bool | Pro 서버 기능을 사용할 수 있으면 true입니다. |
| signedToken | string | 서명된 활성화 토큰입니다. SDK는 이 토큰을 검증한 뒤 활성화 정보를 신뢰합니다. |
| keyId | string | 서버 키 교체 진단에 사용하는 서명 키 ID입니다. |
| expiresAt | number | 활성화 토큰이 만료되는 Unix millisecond입니다. |
code와 message를 가진 JSON입니다. 자주 볼 수 있는 코드는 BAD_JSON, BAD_REQUEST, LICENSE_NOT_FOUND, LICENSE_PENDING, LICENSE_SUSPENDED, LICENSE_REVOKED, LICENSE_EXPIRED, DEVICE_BLOCKED, SERVER_NOT_CONFIGURED, SIGN_FAILED입니다. SDK는 라이선스/기기/identity가 명시적으로 거부된 경우와 일시적인 네트워크 실패를 다르게 처리합니다.
서버 API 키로 게임 서버 연동하기
[Pro] 서버 API 키로 게임 서버가 OZero와 세 가지 일을 합니다. 로그인할 때 플레이어의 빌드가 정품인지 확인(validate), CS·운영을 위해 사용자와 기기 조회(read), 사용자 차단과 해제(block)입니다. 키는 게임 서버에만 둡니다. 게임 클라이언트는 키를 갖지 않으며, OZero SDK를 실행하고 토큰을 고객 서버로 보내기만 합니다.
한눈에 보기
Game client (OZero SDK) Your game server OZero API
----------------------- ---------------- ---------
sign-in request -------> issue audience, challenge, sessionId
RequestGameServerAttestation()
TokenReady: attestToken ------> POST /v1/validate [validate] ----> valid / reason
allow or refuse the sign-in
SetExternalUserId(uuid) (the SDK sends only the hash h1 with policy checks)
support tool: GET /customer-api/... [read] ---->
sanctions: POST/DELETE user-blocks [block] ---->
policy check: callback or end <--------------------------------------------- rule applied
The server API key lives only on your game server. Never put it in the game client.
| 하고 싶은 일 | 범위 | 엔드포인트 |
|---|---|---|
| 접속할 때 빌드가 정품인지 확인 | validate | POST /v1/validate |
| CS: 신고된 사용자가 쓴 기기, 최근 탐지, 특정 기기에 그 사용자가 연결됐는지 | read | GET /v1/customer-api/... |
| 치팅 사용자 차단, 기간 제재, 해제 | block(조회하려면 + read) | POST / DELETE /v1/customer-api/user-blocks |
| CI에서 릴리스 빌드의 보안 설정 해시 등록 | config | GET / POST / DELETE /v1/customer-api/config-hashes |
서버 API 키 권한 범위
이 API는 기존 서버 API 키(Customer Portal > 서버 키 > 서버 API 키)에 권한 범위를 붙여 사용합니다. 키를 발급할 때 범위를 고르며, 키 목록에 키마다 범위가 표시됩니다. 이전에 발급한 키는 validate만 가집니다.
validate:/v1/validate로 토큰 검증(기존 용도).read: 사용자, 기기, 이벤트, 차단 규칙 조회(모든GET요청).block: 사용자 차단 규칙 만들기·갱신·해제(사용자의 게임을 끝내는 강제 종료 규칙 포함).config: 보안 설정 해시 조회·등록·해제(/v1/customer-api/config-hashes, 모든 메서드). CI용입니다. CI 키에는config만 주고block은 주지 마세요.
발급 화면에서 block을 체크하면 read도 함께 체크됩니다(다시 해제할 수 있습니다). block만 있으면 차단 규칙을 만들고 해제할 수는 있지만 사용자·기기·규칙 목록은 조회할 수 없습니다. 조회까지 하려면 read + block을 쓰세요.
block 범위가 있는 키는 강제 종료를 포함해 사용자를 차단할 수 있습니다. 서버 API 키는 고객 서버에만 두고(예: 환경 변수), 게임 클라이언트에는 절대 넣지 마세요.
인증과 제한
모든 요청은 https://api.ozerosecurity.com/v1로 보내고 Authorization: Bearer <서버 API 키>를 붙이며, 활성 Pro 라이선스만 쓸 수 있습니다. POST /v1/validate는 validate 범위가 필요하고 IP별 제한이 있습니다. /v1/customer-api/*는 GET에 read, 규칙 변경에 block이 필요하며(예외: /v1/customer-api/config-hashes는 GET을 포함한 모든 메서드에 config가 필요합니다), 키당 분당 120회와 IP별 제한이 함께 적용됩니다. 목록은 limit와 before(이전 페이지의 nextBefore)로 페이지를 나눕니다.
1. 검증(validate): 정품 빌드인가?
SDK가 빌드 무결성 증거를 OZero로 보내고, 고객 서버가 정한 값에 묶인 일회용 OZA 어테스테이션 토큰을 받습니다. POST /v1/validate는 그 토큰이 이 라이선스의 정품 빌드에 대해 OZero가 이번 로그인용으로 발급했고 아직 쓰이지 않았다는 것을 고객 서버에 알려 줍니다. 로그인, 랭크전, 결제 전에 쓰세요. Online Services > Build Integrity > Check Integrity With Server를 켜고 Attestation Verification Mode를 CustomerGameServer로 설정해야 합니다.
- 게임 서버가 이번 로그인용 바인딩을 발급합니다.
audience(게임 고유 이름, 영문자·숫자·._:-3~128자),challenge(매번 새로 만든 32바이트 난수를 hex 64자로),sessionId(같은 문자 16~128자)입니다. - 클라이언트가
OZeroOnlineAttestation.RequestGameServerAttestation(audience, challenge, sessionId)를 호출합니다. 위 설정이 꺼져 있거나, 로컬 검사가 아직 통과하지 않았거나, 실행 중이거나, 값이 잘못되면 false를 돌려줍니다. - 토큰은
OZeroOnlineAttestation.TokenReady(또는RegisterTokenCallback)로 도착합니다. 클라이언트는context.AttestToken을 게임 서버로 보냅니다. - 게임 서버가
validate범위가 있는 키로POST /v1/validate를 호출합니다. 본문은{ attestToken, licenseKey, audience, challenge, sessionId, enforceVersionWhitelist }이며, 자신이 발급한 값을 그대로 씁니다. valid: true면 로그인을 허용합니다.payload에서 플랫폼, SDK·앱 버전, 매니페스트 해시 등을 알 수 있습니다.valid: false면 거부하고reason을 기록합니다. 토큰은 한 번만 검증할 수 있으며, 두 번째 호출은TOKEN_REPLAYED를 돌려줍니다.
게임 클라이언트(Unity):
using OZeroSDK.Online;
using OZeroSDK.Security;
using UnityEngine;
public sealed class OZeroSignInAttestation : MonoBehaviour
{
private void OnEnable() => OZeroOnlineAttestation.RegisterTokenCallback(OnToken);
private void OnDisable() => OZeroOnlineAttestation.UnregisterTokenCallback(OnToken);
// 1) Your game server answered the sign-in request with a binding.
public void OnBindingReceived(string audience, string challenge, string sessionId)
{
if (!OZeroOnlineAttestation.RequestGameServerAttestation(audience, challenge, sessionId))
Debug.LogWarning("OZero attestation did not start (check Security Config > Build Integrity > Pro - Online).");
}
// 2) The SDK issued a token bound to that binding: send it to your game server.
private void OnToken(OZeroAttestationTokenContext context)
{
MyGameServer.SendAttestToken(context.AttestToken); // your networking code
}
}
요청과 응답:
POST https://api.ozerosecurity.com/v1/validate
Authorization: Bearer <server API key with the validate scope>
Content-Type: application/json
{
"attestToken": "<header>.<payload>.<signature>",
"licenseKey": "<your license key>",
"audience": "my-game-login",
"challenge": "<64 hex characters, new for every sign-in>",
"sessionId": "4b1d6c2e-8f3a-4c55-9a0e-2f1b7d9c3e11",
"enforceVersionWhitelist": false
}
200 OK (genuine build)
{
"valid": true,
"reason": null,
"payload": {
"licenseSubject": "...", "platform": "...", "sdkVersion": "1.2.0", "appVersion": "...",
"manifestHash": "...", "integrityFlags": "...", "deviceBindingHash": "...",
"deviceBinding": null, "installSource": null, "countryCode": null,
"issuedAt": "...", "expiresAt": "...", "tokenId": "..."
}
}
403 Forbidden (rejected)
{ "valid": false, "reason": "TOKEN_REPLAYED", "payload": null }
/v1/validate의 주요 결과:
4xx·5xx 응답에도 JSON 본문이 있습니다. 오류를 던지기 전에 본문을 읽으세요. 키·요청 오류는 { "code", "message" }, 거부된 토큰은 { "valid": false, "reason" }를 돌려줍니다.
| HTTP | code / reason | 설명 |
|---|---|---|
| 401 | SERVER_API_KEY_REQUIRED | 키가 없거나, 알 수 없거나 폐기된 키이거나, validate 범위가 없는 키입니다. |
| 503 | SERVER_NOT_CONFIGURED | 라이선스에 validate 범위가 있는 활성 서버 API 키가 없습니다(read, block, config만 있는 키는 해당하지 않음). Customer Portal에서 validate 범위로 발급하세요. |
| 400 | BAD_JSON, BAD_REQUEST | 본문이 올바른 JSON이 아니거나 필드 형식이 잘못되었습니다. |
| 403 | TOKEN_REPLAYED | 이미 검증한 토큰입니다. 새 바인딩을 발급하고 클라이언트에 새 토큰을 요청하세요. |
| 403 | AUDIENCE_MISMATCH, CHALLENGE_MISMATCH, SESSION_MISMATCH | 보낸 값으로 발급된 토큰이 아닙니다. 발급한 audience, challenge, sessionId를 그대로 넘겼는지 확인하세요. |
| 400 / 401 | MALFORMED_TOKEN, INVALID_SIGNATURE, TOKEN_EXPIRED | 토큰이 손상되었거나 위조되었거나 만료되었습니다. 로그인을 거부하세요. |
| 403 | MANIFEST_HASH_NOT_WHITELISTED, BUILD_BLOCKED, COUNTRY_BLOCKED, DEVICE_MISMATCH | 라이선스 정책이 빌드나 기기를 거부했습니다(enforceVersionWhitelist를 켠 버전 화이트리스트, 차단된 빌드, 차단된 국가, 기기 불일치). |
| 403 | LICENSE_*, TIER_REQUIRED | 라이선스를 쓸 수 없는 상태입니다(정지, 만료, 폐기, 대기, 또는 Pro가 아님). |
2. 조회(read): 사용자와 기기 찾기
CS에서 자주 묻는 것: 신고된 사용자가 어떤 기기로 플레이했는지, OZero가 최근 무엇을 탐지했는지, 신고서의 기기에 이 사용자가 연결되어 있는지입니다. 사용자 UUID와 라이선스 salt로 h1을 계산한 뒤 아래 GET 엔드포인트를 호출하세요. 서버는 키 해시만 저장하므로 사용자의 h1 목록은 줄 수 없습니다. 기기에서 특정 사용자를 확인하려면 ?h1=을 넘기세요.
| 엔드포인트 | 범위 | 용도 |
|---|---|---|
| GET /customer-api/users/{h1}/devices | read | 그 사용자가 관측된 기기(처음·마지막 관측, 이벤트 수)와 현재 적용 중인 규칙(block, 없으면 null). |
| GET /customer-api/users/{h1}/events | read | 그 사용자의 최근 이벤트(최신순). source는 telemetry, physics, policy 중 하나입니다. from / to(Unix ms)로 기간을 거릅니다. |
| GET /customer-api/devices/{deviceId}/users | read | 기기에 연결된 사용자: count와 사용자별 키 앞 8자. 서버는 h1을 저장하지 않으므로 사용자 해시 목록은 줄 수 없습니다. ?h1=을 넘기면 그 사용자가 이 기기에 연결됐는지를 linked(true/false)로 돌려줍니다. |
| GET /customer-api/user-blocks | read | 차단 규칙 목록(최신순). ?active=true면 적용 중인 규칙(해제·만료되지 않은 것)만 돌려줍니다. |
GET /users/{h1}/devices 응답 예:
{
"userKeyPrefix": "a1b2c3d4",
"devices": [
{ "deviceId": "...", "firstSeenAt": 1789900000000, "lastSeenAt": 1790000000000, "eventCount": 12 }
],
"block": null
}
3. 차단(block): 제재와 해제
주로 이렇게 씁니다: 치팅이 확정되면 자동으로 차단, 운영 툴에서 기간 제재, 이의 신청이 받아들여지면 해제. callback은 대응을 게임 코드에 맡기고, terminate는 게임을 끝냅니다. 같은 사용자에 다시 보내면 적용 중인 규칙이 갱신됩니다.
| 엔드포인트 | 범위 | 용도 |
|---|---|---|
| POST /customer-api/user-blocks | block | 규칙 만들기: { "h1", "action": "callback" | "terminate", "reason", "expiresInDays" 또는 "expiresAt" }. 만료를 주지 않으면 해제할 때까지 유지됩니다. 사용자당 적용 중인 규칙은 하나이므로 같은 사용자에 다시 보내면 갱신됩니다(201 대신 200). |
| DELETE /customer-api/user-blocks/{id} | block | 규칙을 해제합니다. 여러 번 호출해도 문제없습니다. |
게임에서 일어나는 일: 게임이 SetExternalUserId를 호출하고 약 5초 뒤(최소 30초 간격) 기기 정책 확인에서 규칙이 적용됩니다. callback은 Module = "user_policy"인 policy action을 게임에 보내고, terminate는 policy action을 먼저 보낸 뒤 OZ-SEC-0E 안내를 보여 주고 세션을 끝내며 OZeroLicenseRuntime.LastServerBlockCode가 "USER_BLOCKED"가 됩니다. 매뉴얼의 기기 ID와 사용자 UUID를 참고하세요.
4. 설정(config): 보안 설정 해시(CI)
릴리스 빌드마다 해시를 등록하면 보안 레벨 확인이 고객 빌드만 통과시킵니다(매뉴얼의 보안 설정 해시 허용 목록 참고). 모든 메서드에 config 범위가 필요합니다. 해시는 빌드 로그 줄 [OZero] Security config digest for this <Target> build: …의 소문자 hex 64자입니다.
| 엔드포인트 | 범위 | 용도 |
|---|---|---|
| GET /customer-api/config-hashes | config | 정책(mode: enforced 또는 compatibility, activeCount), 승인된 해시(status: active, expired, revoked), 서버가 받은 해시(observed: 처음·마지막 관측, 관측 수, 선언 레벨, SDK 버전, 플랫폼, approval). |
| POST /customer-api/config-hashes | config | 해시 승인: { "configHash", "label"?, "expiresInDays"? 또는 "expiresAt"?, "acknowledgeEnforcement"? }. { hash, created, enforcementStarted }를 돌려주며, 새 해시는 201, 기존 해시를 다시 승인하면 200입니다. 만료를 주지 않으면 해제할 때까지 유지됩니다. |
| DELETE /customer-api/config-hashes/{configHash} | config | 해시를 해제합니다. { hash, policy }를 돌려주며, 마지막 유효 해시를 해제하면 policy.mode가 compatibility가 됩니다. 다시 해제해도 문제없습니다. |
"acknowledgeEnforcement": true가 있어야 하며, 없으면 서버가 409 ENFORCEMENT_ACK_REQUIRED로 답합니다. 배포 중인 빌드를 모두 확인한 뒤 첫 승인은 Customer Portal에서 하고, 이후 빌드는 CI가 등록하도록 하는 것을 권장합니다.
요청과 응답:
POST https://api.ozerosecurity.com/v1/customer-api/config-hashes
Authorization: Bearer <server API key with the config scope>
Content-Type: application/json
{
"configHash": "<64 hex characters from the build log>",
"label": "1.2.0 release (Windows)",
"expiresInDays": 365,
"acknowledgeEnforcement": true
}
201 Created
{
"hash": {
"configHash": "...", "label": "1.2.0 release (Windows)", "status": "active",
"createdAt": 1790000000000, "createdBy": "api-key:...", "expiresAt": 1821536000000,
"revokedAt": null, "revokedBy": null
},
"created": true,
"enforcementStarted": true
}
409 Conflict (first approval without acknowledgeEnforcement)
{ "code": "ENFORCEMENT_ACK_REQUIRED", "message": "the first approved hash turns on enforcement: ..." }
GET https://api.ozerosecurity.com/v1/customer-api/config-hashes
{
"policy": { "activeCount": 1, "mode": "enforced" },
"approved": [ { "configHash": "...", "label": "...", "status": "active", "expiresAt": null, "...": "..." } ],
"observed": [ { "configHash": "...", "firstSeenAt": 1789900000000, "lastSeenAt": 1790000000000, "seenCount": 3,
"lastDeclaredLevel": 2, "lastSdkVersion": "1.2.0", "lastPlatform": "...", "approval": "active" } ],
"schemaReady": true
}
DELETE https://api.ozerosecurity.com/v1/customer-api/config-hashes/<configHash>
{ "hash": { "configHash": "...", "status": "revoked", "...": "..." },
"policy": { "activeCount": 0, "mode": "compatibility" } }
CI 예시(bash, Unity 빌드 다음 단계): 빌드 로그(-logFile로 지정한 파일)에서 해시를 읽어 등록합니다. 키는 CI 시크릿에 저장하세요.
#!/usr/bin/env bash
# CI step after the Unity build. BUILD_LOG is the file passed to Unity with -logFile.
# OZERO_CONFIG_API_KEY: a server API key with only the config scope, stored as a CI secret.
set -euo pipefail
BUILD_LOG="${BUILD_LOG:-build.log}"
LABEL="${BUILD_LABEL:-ci build}" # letters, digits and spaces only (it goes into JSON as is)
HASH=$(grep -oE 'Security config digest for this [A-Za-z0-9]+ build: [0-9a-f]{64}' "$BUILD_LOG" \
| tail -n 1 | awk '{print $NF}')
if [ -z "$HASH" ]; then
echo "No security config digest in $BUILD_LOG" >&2
exit 1
fi
STATUS=$(curl -sS -o ozero-response.json -w '%{http_code}' -X POST \
"https://api.ozerosecurity.com/v1/customer-api/config-hashes" \
-H "Authorization: Bearer ${OZERO_CONFIG_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"configHash\":\"${HASH}\",\"label\":\"${LABEL}\",\"expiresInDays\":365}")
case "$STATUS" in
200|201) echo "Approved security config hash ${HASH}" ;;
409) echo "First approval turns on enforcement: approve once in the Customer Portal," \
"or send \"acknowledgeEnforcement\": true on purpose." >&2
cat ozero-response.json >&2; exit 1 ;;
*) echo "OZero API returned HTTP ${STATUS}:" >&2 # the body has { "code", "message" }
cat ozero-response.json >&2; exit 1 ;;
esac
| HTTP | code | 설명 |
|---|---|---|
| 400 | BAD_CONFIG_HASH, BAD_EXPIRY, BAD_JSON | configHash가 hex 64자가 아니거나 만료 값이 잘못되었습니다. |
| 403 | SCOPE_REQUIRED | 키에 config 범위가 없습니다. |
| 404 | CONFIG_HASH_NOT_FOUND | 이 라이선스 목록에 없는 해시입니다(DELETE). |
| 409 | ENFORCEMENT_ACK_REQUIRED | acknowledgeEnforcement: true 없이 첫 승인을 요청했습니다. |
| 503 | CONFIG_HASH_SCHEMA_NOT_READY | 서버가 아직 보안 설정 해시를 처리할 준비가 되지 않았습니다. 잠시 뒤 다시 시도하세요. |
고객 API 오류
4xx·5xx 응답도 본문에 { "code", "message" }를 돌려줍니다. 본문을 읽고 code로 분기하세요.
| HTTP | code | 설명 |
|---|---|---|
| 401 | API_KEY_REQUIRED, API_KEY_INVALID | Authorization 헤더가 없거나, 키를 알 수 없거나 폐기되었습니다. |
| 403 | SCOPE_REQUIRED | 키에 요청에 필요한 범위(read, block 또는 config)가 없습니다. |
| 403 | PRO_REQUIRED | 라이선스가 Pro가 아닙니다. |
| 403 | LICENSE_SUSPENDED, LICENSE_EXPIRED, LICENSE_REVOKED, LICENSE_PENDING | 라이선스가 정지, 만료, 폐기 또는 대기 상태입니다. |
| 400 | BAD_* | 잘못된 요청입니다. 예: BAD_USER_HASH(h1이 소문자 hex 64자가 아님), BAD_ACTION, BAD_EXPIRY, BAD_DEVICE_ID, BAD_JSON. |
| 404 | NOT_FOUND | 이 라이선스에 그런 규칙이 없습니다. |
| 429 | RATE_LIMITED | 요청이 너무 많습니다(키별 또는 IP별). 잠시 뒤 다시 시도하세요. |
| 503 | EXTERNAL_USER_DISABLED | 지금 서버에서 사용자 UUID 연동을 쓸 수 없습니다. |
API로 만든 규칙은 Customer Portal 규칙 목록에 고객 서버 API로 표시되고, 운영 이력에는 server-api-key …와 키 끝 4자리로 기록됩니다.
5. 예제: 게임 서버 파일 하나로 따라 하기
validate, read, block을 모두 다루는 Node.js 모듈 전체(18 이상, 추가 패키지 없음)와, 같은 흐름의 C#·Python 예제입니다. 키, 라이선스 키, salt는 환경 변수에서 읽습니다. 해시 함수는 쓰기 전에 테스트 벡터로 스스로 확인합니다. 429와 503은 지수 백오프로 재시도합니다.
// ozero-server.js - Node.js 18+ (built-in fetch and crypto, no extra packages)
const crypto = require("crypto");
const API = "https://api.ozerosecurity.com/v1";
const VALIDATE_KEY = process.env.OZERO_VALIDATE_API_KEY; // scope: validate (sign-in servers)
const OPS_KEY = process.env.OZERO_SERVER_API_KEY; // scopes: read + block (operations tools)
const LICENSE_KEY = process.env.OZERO_LICENSE_KEY;
const SALT = process.env.OZERO_EXTUID_SALT; // Customer Portal > User UUID > Settings
const AUDIENCE = "my-game-login";
// h1 = SHA-256("OZERO-EXTUID-V1|" + salt + "|" + uuid) as lowercase hex.
function extUserHash(salt, uuid) {
const id = uuid.replace(/^[ \t\n\v\f\r]+|[ \t\n\v\f\r]+$/g, "");
return crypto.createHash("sha256").update("OZERO-EXTUID-V1|" + salt + "|" + id, "utf8").digest("hex");
}
// Self-check with the published test vector.
const TEST_SALT = Array.from({ length: 32 }, (_, i) => i.toString(16).padStart(2, "0")).join("");
if (extUserHash(TEST_SALT, "player-123") !== "2d5a23607064e6dbc02dc32dc96af8cf54862d2f51d778596cfbe9d6ba8f8a3a") {
throw new Error("extUserHash does not match the test vector");
}
// 4xx and 5xx responses carry a JSON body with "code" (for example SCOPE_REQUIRED).
class OZeroApiError extends Error {
constructor(status, data) {
super(`OZero API ${status}: ${data.code ?? "HTTP_" + status} ${data.message ?? ""}`.trim());
this.status = status;
this.code = data.code ?? `HTTP_${status}`;
}
}
// One request helper: reads the JSON body for every status and retries 429 and 503
// with exponential backoff (0.5, 1, 2, 4 s).
async function call(key, method, path, body, attempt = 0) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if ((res.status === 429 || res.status === 503) && attempt < 4) {
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
return call(key, method, path, body, attempt + 1);
}
return { status: res.status, data: await res.json().catch(() => ({})) };
}
async function callOk(key, method, path, body) {
const { status, data } = await call(key, method, path, body);
if (status >= 400) throw new OZeroApiError(status, data);
return { status, data };
}
// 1) validate --------------------------------------------------------------
function newSignInBinding() {
return {
audience: AUDIENCE,
challenge: crypto.randomBytes(32).toString("hex"),
sessionId: crypto.randomUUID(),
};
}
async function validateSignIn(binding, attestToken) {
const { status, data } = await call(VALIDATE_KEY, "POST", "/validate", {
attestToken,
licenseKey: LICENSE_KEY,
...binding,
enforceVersionWhitelist: false,
});
if (data.valid === true) return { ok: true, payload: data.payload };
if (data.valid === false) return { ok: false, reason: data.reason }; // e.g. TOKEN_REPLAYED: refuse
throw new OZeroApiError(status, data); // e.g. SERVER_API_KEY_REQUIRED: fix the key or setup
}
// 2) read ------------------------------------------------------------------
async function lookUpUser(uuid) {
const h1 = extUserHash(SALT, uuid);
const devices = await callOk(OPS_KEY, "GET", `/customer-api/users/${h1}/devices`);
const events = await callOk(OPS_KEY, "GET", `/customer-api/users/${h1}/events?limit=20`);
return { devices: devices.data.devices, block: devices.data.block, events: events.data.events };
}
async function isUserOnDevice(uuid, deviceId) {
const h1 = extUserHash(SALT, uuid);
const { data } = await callOk(OPS_KEY, "GET",
`/customer-api/devices/${encodeURIComponent(deviceId)}/users?h1=${h1}`);
return data.linked === true;
}
// 3) block -----------------------------------------------------------------
async function blockUser(uuid, action, reason, days) {
const body = { h1: extUserHash(SALT, uuid), action, reason }; // action: "callback" | "terminate"
if (days) body.expiresInDays = days; // omit to keep it until lifted
const { data } = await callOk(OPS_KEY, "POST", "/customer-api/user-blocks", body);
return data.block.id; // 201 created, 200 updated the user's rule in effect
}
async function liftBlock(ruleId) {
await callOk(OPS_KEY, "DELETE", `/customer-api/user-blocks/${ruleId}`);
}
// Branch on the error code, for example in your operations tool:
async function blockFromTool(uuid) {
try {
return await blockUser(uuid, "terminate", "Cheating", 30);
} catch (e) {
if (e.code === "SCOPE_REQUIRED") throw new Error("This key has no block scope: issue a read + block key.");
if (e.code === "API_KEY_INVALID") throw new Error("The key was revoked: deploy the new key.");
throw e;
}
}
module.exports = {
newSignInBinding, validateSignIn, lookUpUser, isUserOnDevice, blockUser, liftBlock, blockFromTool, OZeroApiError,
};
// C# (.NET 6+): the same flow
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
// 4xx and 5xx responses carry a JSON body with "code" (for example SCOPE_REQUIRED).
public sealed class OZeroApiException : Exception
{
public OZeroApiException(int status, string code, string message)
: base($"OZero API {status}: {code} {message}".Trim())
{
Status = status;
Code = code;
}
public int Status { get; }
public string Code { get; }
}
public sealed class OZeroServerClient
{
// Leave out null fields such as expiresInDays when no expiry is given.
private static readonly JsonSerializerOptions Json = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
private static readonly Uri Api = new("https://api.ozerosecurity.com/v1/");
private readonly HttpClient _validate = Create(Env("OZERO_VALIDATE_API_KEY"));
private readonly HttpClient _ops = Create(Env("OZERO_SERVER_API_KEY"));
private readonly string _licenseKey = Env("OZERO_LICENSE_KEY");
private readonly string _salt = Env("OZERO_EXTUID_SALT");
private static string Env(string name) =>
Environment.GetEnvironmentVariable(name) ?? throw new InvalidOperationException(name + " is not set");
private static HttpClient Create(string key)
{
var http = new HttpClient { BaseAddress = Api };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);
return http;
}
public static string ExtUserHash(string salt, string uuid)
{
var id = uuid.Trim(' ', '\t', '\n', '\v', '\f', '\r');
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes("OZERO-EXTUID-V1|" + salt + "|" + id));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
// Sends the request (retrying 429 and 503 with exponential backoff) and always reads the JSON body.
private static async Task<(int Status, JsonElement Body)> Send(Func<Task<HttpResponseMessage>> request)
{
for (int attempt = 0; ; attempt++)
{
using var res = await request();
bool retry = res.StatusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.ServiceUnavailable;
if (retry && attempt < 4)
{
await Task.Delay(500 * (1 << attempt));
continue;
}
JsonElement body;
try { body = await res.Content.ReadFromJsonAsync<JsonElement>(); }
catch (JsonException) { body = default; } // not JSON (for example a proxy error page)
return ((int)res.StatusCode, body);
}
}
// Throws OZeroApiException with the server's code for 4xx and 5xx.
private static JsonElement Ok((int Status, JsonElement Body) r)
{
if (r.Status < 400) return r.Body;
string code = $"HTTP_{r.Status}", message = "";
if (r.Body.ValueKind == JsonValueKind.Object)
{
if (r.Body.TryGetProperty("code", out var c)) code = c.GetString() ?? code;
if (r.Body.TryGetProperty("message", out var m)) message = m.GetString() ?? "";
}
throw new OZeroApiException(r.Status, code, message);
}
// 1) validate: (true, payload) allows the sign-in, (false, reason) refuses it.
public async Task<(bool Ok, JsonElement Result)> ValidateSignIn(
string audience, string challenge, string sessionId, string attestToken)
{
var r = await Send(() => _validate.PostAsJsonAsync("validate", new
{
attestToken, licenseKey = _licenseKey, audience, challenge, sessionId, enforceVersionWhitelist = false,
}));
if (r.Body.ValueKind == JsonValueKind.Object && r.Body.TryGetProperty("valid", out var valid))
return valid.GetBoolean() ? (true, r.Body.GetProperty("payload")) : (false, r.Body.GetProperty("reason"));
Ok(r); // no "valid": a key or setup error such as SERVER_API_KEY_REQUIRED
throw new OZeroApiException(r.Status, $"HTTP_{r.Status}", "unexpected response");
}
// 2) read
public async Task<JsonElement> LookUpUser(string uuid) =>
Ok(await Send(() => _ops.GetAsync($"customer-api/users/{ExtUserHash(_salt, uuid)}/devices")));
// 3) block and lift
public async Task<string> BlockUser(string uuid, string action, string reason, int? days = null)
{
var body = Ok(await Send(() => _ops.PostAsJsonAsync("customer-api/user-blocks", new
{
h1 = ExtUserHash(_salt, uuid), action, reason, expiresInDays = days,
}, Json)));
return body.GetProperty("block").GetProperty("id").GetString()!;
}
public async Task LiftBlock(string ruleId) =>
Ok(await Send(() => _ops.DeleteAsync($"customer-api/user-blocks/{ruleId}")));
// Branch on the error code, for example in your operations tool.
public async Task<string?> BlockFromTool(string uuid)
{
try
{
return await BlockUser(uuid, "terminate", "Cheating", 30);
}
catch (OZeroApiException e) when (e.Code == "SCOPE_REQUIRED")
{
Console.Error.WriteLine("This key has no block scope: issue a read + block key.");
return null;
}
}
}
# Python 3 + requests: the same flow
import hashlib
import os
import secrets
import time
import uuid as uuidlib
import requests
API = "https://api.ozerosecurity.com/v1"
VALIDATE_KEY = os.environ["OZERO_VALIDATE_API_KEY"] # scope: validate
OPS_KEY = os.environ["OZERO_SERVER_API_KEY"] # scopes: read + block
LICENSE_KEY = os.environ["OZERO_LICENSE_KEY"]
SALT = os.environ["OZERO_EXTUID_SALT"]
def ext_user_hash(salt: str, uuid: str) -> str:
uid = uuid.strip(" \t\n\v\f\r")
return hashlib.sha256(f"OZERO-EXTUID-V1|{salt}|{uid}".encode("utf-8")).hexdigest()
assert ext_user_hash(bytes(range(32)).hex(), "player-123") == \
"2d5a23607064e6dbc02dc32dc96af8cf54862d2f51d778596cfbe9d6ba8f8a3a"
class OZeroApiError(Exception):
"""4xx and 5xx responses carry a JSON body with "code" (for example SCOPE_REQUIRED)."""
def __init__(self, status, data):
self.status = status
self.code = data.get("code") or f"HTTP_{status}"
super().__init__(f"OZero API {status}: {self.code} {data.get('message', '')}".strip())
def call(key, method, path, body=None):
"""Reads the JSON body for every status; retries 429 and 503 with exponential backoff."""
for attempt in range(5):
res = requests.request(method, API + path, json=body, timeout=10,
headers={"Authorization": f"Bearer {key}"})
if res.status_code not in (429, 503) or attempt == 4:
try:
data = res.json()
except ValueError:
data = {}
return res.status_code, data
time.sleep(0.5 * 2 ** attempt)
def call_ok(key, method, path, body=None):
status, data = call(key, method, path, body)
if status >= 400:
raise OZeroApiError(status, data)
return data
# 1) validate
def new_sign_in_binding():
return {"audience": "my-game-login", "challenge": secrets.token_hex(32), "sessionId": str(uuidlib.uuid4())}
def validate_sign_in(binding, attest_token):
status, data = call(VALIDATE_KEY, "POST", "/validate", {
"attestToken": attest_token, "licenseKey": LICENSE_KEY, **binding, "enforceVersionWhitelist": False})
if "valid" not in data:
raise OZeroApiError(status, data) # e.g. SERVER_API_KEY_REQUIRED
return data["valid"], data.get("payload") if data["valid"] else data.get("reason")
# 2) read
def look_up_user(uuid):
return call_ok(OPS_KEY, "GET", f"/customer-api/users/{ext_user_hash(SALT, uuid)}/devices")
# 3) block and lift
def block_user(uuid, action, reason, days=None):
body = {"h1": ext_user_hash(SALT, uuid), "action": action, "reason": reason}
if days:
body["expiresInDays"] = days
return call_ok(OPS_KEY, "POST", "/customer-api/user-blocks", body)["block"]["id"]
def lift_block(rule_id):
call_ok(OPS_KEY, "DELETE", f"/customer-api/user-blocks/{rule_id}")
# Branch on the error code, for example in your operations tool:
def block_from_tool(uuid):
try:
return block_user(uuid, "terminate", "Cheating", 30)
except OZeroApiError as e:
if e.code == "SCOPE_REQUIRED":
raise RuntimeError("This key has no block scope: issue a read + block key.") from e
raise
6. 운영 팁
- 용도별로 키를 나누세요. 로그인 서버에는
validate키, 운영 툴에만read+block키를 둡니다. 그러면 로그인 키가 새어도 사용자를 차단할 수 없습니다. - CI에는
config범위만 있는 전용 키를 주세요. 그러면 CI 키가 새어도 사용자를 조회하거나 차단할 수 없습니다. - 중단 없이 키 교체: 새 키를 발급하고 서버에 배포한 뒤, Customer Portal에서 옛 키를 폐기합니다.
- 제한에 유의하세요. 고객 API는 키당 분당 120회입니다. CS 조회는 묶어서 처리하고 429에는 백오프하세요.
- 서버 API 키, salt,
h1, 원래 UUID를 로그나 분석 데이터에 남기지 마세요.h1은 가명 개인 식별자로 다루세요.
OZeroAbortCode 및 이벤트 메시지
OZero가 보안 위협을 확정하면 OZeroSecurityEvent를 만들고, SDK 기본 대응 흐름과 프로젝트에서 등록한 콜백에 전달합니다. 이벤트에는 ModulationType, 안정적인 공개 OZeroAbortCode, MessageKey, 안전한 영문 Message, WillAbort가 포함됩니다.
중단 코드 및 메시지 표
| 코드 | OZeroAbortCode | ModulationType | MessageKey | 메시지 |
|---|---|---|---|---|
| 0x01 | MemoryModulation | MemoryModulation | memory_modulation | Protected memory value changed unexpectedly. |
| 0x02 | Injection | Injection | injection | Unexpected module, hook, or runtime injection signal detected. |
| 0x0A | BuildIntegrity | BuildIntegrity | build_integrity | Build integrity validation failed. |
| 0x0C | SpeedOrTimeHack | SpeedHack | speed_hack | Suspicious time scale or execution speed change detected. |
| 0x0C | SpeedOrTimeHack | TimeHack | time_hack | System clock or trusted time anomaly detected. |
| 0x0E | DeviceOrInstallPolicy | DeviceBindingModulation | device_binding | Device binding policy rejected the current device. |
| 0x0E | DeviceOrInstallPolicy | InstallSource | install_source | Application install source is not trusted. |
| 0x0F | PhysicsHack | PhysicsHack | physics_hack | Abnormal physics behavior exceeded the configured policy. |
| 0x10 | EnvironmentModulation | EnvironmentModulation | environment_modulation | Unsupported or unsafe runtime environment detected. |
| 0x13 | SteamAntiPiracy | SteamAntiPiracy | steam_antipiracy | Steam ownership or ticket validation failed. |
| 0x15 | InjectionScannerUnavailable | InjectionScannerUnavailable | injection_scanner_unavailable | 인젭션 스캐너가 동작하지 못해 SDK가 무방비 실행 대신 게임을 종료했습니다. ForceQuitOnDetection이 꺼져 있어도 발생합니다. |
로그나 다국어 UI를 만들 때는 OZeroAbortCode와 MessageKey를 기준값으로 사용하세요. Message는 개발자가 상황을 이해할 수 있도록 안전한 표현으로 정리되어 있어, 개발자용 화면이나 QA 로그에 그대로 표시해도 괜찮습니다.
런타임에서 보안 이벤트 처리
기본적으로 OZero는 Config Dashboard의 Response 설정에 따라 앱을 종료하거나 로그만 남깁니다. 직접 경고 화면을 표시하거나, 자체 서버 로그를 보내거나, 종료 직전에 짧은 저장 처리가 필요하다면 OZeroSecurityManager.RegisterUserCallback으로 핸들러를 등록하세요.
evt.WillAbort가 true이면 현재 대응 정책상 콜백 흐름 이후 앱이 종료됩니다. 이때는 analytics flush나 마지막 저장처럼 짧게 끝나는 작업만 수행하세요. 콜백은 이벤트를 보고하고 마무리 처리를 연결하는 지점이며, OZero의 보안 대응을 취소하는 용도가 아닙니다.
using OZeroSDK.Security;
void OnEnable()
{
OZeroSecurityManager.Instance.RegisterUserCallback(OnHack);
}
void OnHack(OZeroSecurityEvent evt)
{
Debug.LogWarning(
$"OZero: {evt.Type} {evt.AbortCodeHex} {evt.MessageKey} - {evt.Message}");
if (evt.WillAbort)
{
// Last chance to flush your own analytics or save state.
}
Analytics.FlushSync();
}
Injection Detector API OZeroSDK.Security
Injection 검사에서 정상 모듈로 인정할 항목을 등록하는 API입니다. 예를 들어 게임과 함께 배포하는 overlay, 녹화 도구, 운영용 플러그인처럼 정상 동작하지만 Injection 검사에 잡힐 수 있는 모듈을 예외로 둘 때 사용합니다. HashHex는 해당 모듈 파일의 SHA-256 hash이고, SignerHex는 모듈 서명 인증서의 SHA-256 hash입니다. 값을 추측해서 입력하지 말고, 실제 배포 파일이나 진단 결과에서 확인한 값만 등록하세요.
데이터 구조 — OZeroInjectionWhitelistEntry
[Serializable]
public class OZeroInjectionWhitelistEntry
{
// SHA-256 of the matched module file. Lowercase 64-char hex. Required.
public string HashHex { get; set; }
// SHA-256 of the module's signing certificate. Lowercase 64-char hex.
// Empty ("") means "match by hash only" (only mode for Android .so / Linux ELF).
public string SignerHex { get; set; }
// Module file format hint — "pe" | "macho" | "so". Defaults to "so".
public string Type { get; set; }
// Optional human-readable note (UI / audit only — never sent to native).
public string Comment { get; set; }
}
Unity에서 로컬 신뢰 모듈 항목을 등록할 때 사용하는 데이터 구조입니다. HashHex는 필수 64자리 SHA-256 파일 hash이고, SignerHex는 선택 64자리 signer fingerprint입니다. Type은 pe, macho, so 중 하나로 모듈 형식을 알려주며, Comment는 운영자가 알아볼 수 있는 메모입니다.
런타임 API — OZeroDispatch
// Returns true when trusted-module policy support is available.
public static bool HasInjectionV3 { get; }
// Replace trusted module entries atomically. Pass null/empty to clear.
// Returns false when the runtime support is unavailable.
public static bool RegisterInjectionWhitelistHash(OZeroInjectionWhitelistEntry[] entries);
// Trusted-module aware scan. Returns true when a relevant runtime signal is observed.
// Output fields are diagnostic context for your review and may be empty.
public static bool DetectAssemblyInjectionV3(
out bool silencedByWhitelist,
out string hashHex,
out string signerHex,
out string matchedModulePath);
실행 중에 신뢰 모듈 목록을 네이티브 검사기에 전달하거나, 현재 플랫폼에서 이 기능을 사용할 수 있는지 확인할 때 사용합니다. 일반 프로젝트는 Config Dashboard의 Injection 설정만으로 충분합니다. 코드에서 직접 호출해야 한다면 배포 파일이 확정된 뒤 필요한 항목만 등록하고, 등록 전후 결과를 QA 로그로 확인하세요.
Config — OZeroSecurityConfig.InjectionSettings
// Preferred local trusted-module surface in the Injection settings.
public OZeroInjectionWhitelistEntry[] InjectionWhitelistEntries { get; }
public bool EnableWindowsSystemModuleScan { get; }
InjectionWhitelistEntries는 Injection 설정에 포함되는 로컬 신뢰 모듈 목록입니다. 고객 PC에 우연히 설치된 프로그램을 넓게 허용하기 위한 설정이 아니라, 개발사가 함께 배포하고 정상 동작을 확인한 모듈만 등록하는 곳입니다. 이 로컬 목록은 모든 티어에서 사용할 수 있고, Pro 고객은 고객 포털의 서버 관리 whitelist로 운영 중 같은 성격의 정책을 갱신할 수 있습니다.
Injection 설정의 enableWindowsSystemModuleScan 값을 반환하는 읽기 전용 속성입니다. 옵션을 바꾸려면 Unity의 Injection 설정을 조정하거나 Low/Standard/Strict 보안 프리셋을 적용하세요.
처음 보는 시스템 모듈이라고 해서 바로 게임을 종료하지는 않습니다. 우선 관측 대상으로 기록하며, 반복해서 발견되거나 파일에 서명이 없다는 이유만으로 차단하지 않습니다. 기존 허용 규칙은 유지되고, 해시로 명시적으로 차단한 모듈에는 차단 정책이 적용됩니다. 이 옵션은 Unity의 Injection 설정에서 조정할 수 있으며, Pro 포털에서는 원격으로 변경할 수 없습니다. 다른 플랫폼에는 영향을 주지 않습니다.