144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
/*
|
|
* 기기 무결성/위변조 탐지 레이어 (B1 루트·탈옥 / B9 디버거·개발자모드·USB / B2 앱무결성 토큰 전송부).
|
|
* 근거: docs/security/mobile-security-audit.md 백로그 B1·B2·B9, checklist AT-1/AT-4/AT-6/AT-2.
|
|
*
|
|
* 성격(공공 대민 정책): 탐지 시 "경고 후 제한" — 하드 차단이 아니라 사용자 고지.
|
|
* 오탐(에뮬레이터 개발기기·루팅 오검출)으로 정상 이용자를 잠그지 않는다(graceful).
|
|
* 도입 경로: jail-monkey는 네이티브 모듈 → Expo Go/웹/미설치 기기에서는 방어적 동적 require로
|
|
* "탐지 불가(안전 가정)" 취급. dev-client/prebuild(EAS)에서 실제 동작. (biometric.ts 동일 패턴)
|
|
* 시크릿 미보관: 앱은 불린/토큰만 다루고 원본 서명·키를 저장하지 않는다.
|
|
*/
|
|
import { useEffect, useState } from 'react';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let JailMonkey: any = null;
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
JailMonkey = require('jail-monkey');
|
|
// 일부 번들러는 default 래핑 → 안전 정규화
|
|
if (JailMonkey && JailMonkey.default) JailMonkey = JailMonkey.default;
|
|
} catch {
|
|
JailMonkey = null;
|
|
}
|
|
|
|
export interface IntegrityResult {
|
|
/** 네이티브 탐지 모듈 사용 가능 여부(불가 시 아래 값은 모두 안전 기본값 false). */
|
|
available: boolean;
|
|
/** 루팅(Android)·탈옥(iOS) 단말. */
|
|
jailBroken: boolean;
|
|
/** 디버거 attach(Android). */
|
|
debugger: boolean;
|
|
/** 후킹(Frida/Xposed 등, Android). */
|
|
hooking: boolean;
|
|
/** USB 디버깅(ADB) 활성(Android). */
|
|
adbEnabled: boolean;
|
|
/** 개발자 옵션 활성(Android). */
|
|
devMode: boolean;
|
|
/** 위치 모의(mock) 허용. */
|
|
mockLocation: boolean;
|
|
/** 종합 위변조 신호(위 항목 중 하나라도 참). */
|
|
compromised: boolean;
|
|
}
|
|
|
|
const SAFE: IntegrityResult = {
|
|
available: false,
|
|
jailBroken: false,
|
|
debugger: false,
|
|
hooking: false,
|
|
adbEnabled: false,
|
|
devMode: false,
|
|
mockLocation: false,
|
|
compromised: false,
|
|
};
|
|
|
|
/** 동기 불린 호출을 방어적으로 실행(예외/미구현 시 false). */
|
|
function safeBool(fn: (() => boolean) | undefined): boolean {
|
|
if (typeof fn !== 'function') return false;
|
|
try {
|
|
return !!fn();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Promise 반환 API를 방어적으로 실행(예외/미구현 시 false). */
|
|
async function safeBoolAsync(fn: (() => Promise<boolean>) | undefined): Promise<boolean> {
|
|
if (typeof fn !== 'function') return false;
|
|
try {
|
|
return !!(await fn());
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기기 무결성 점검. 네이티브 모듈이 없으면 SAFE(모두 false)로 graceful 반환.
|
|
* 결과는 UI에서 "경고 배너" 등으로 활용(차단 아님).
|
|
*/
|
|
export async function checkDeviceIntegrity(): Promise<IntegrityResult> {
|
|
if (!JailMonkey) return { ...SAFE };
|
|
try {
|
|
const jailBroken = safeBool(JailMonkey.isJailBroken);
|
|
const hooking = safeBool(JailMonkey.hookDetected);
|
|
const adbEnabled = safeBool(JailMonkey.AdbEnabled);
|
|
const mockLocation = safeBool(JailMonkey.canMockLocation);
|
|
const debugger_ = await safeBoolAsync(JailMonkey.isDebuggedMode);
|
|
const devMode = await safeBoolAsync(JailMonkey.isDevelopmentSettingsMode);
|
|
const compromised = jailBroken || hooking || debugger_;
|
|
return {
|
|
available: true,
|
|
jailBroken,
|
|
debugger: debugger_,
|
|
hooking,
|
|
adbEnabled,
|
|
devMode,
|
|
mockLocation,
|
|
compromised,
|
|
};
|
|
} catch {
|
|
return { ...SAFE };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* React 훅 — 마운트 시 1회 무결성 점검(비차단). 로그인 등 진입 화면에서 경고 노출용.
|
|
*/
|
|
export function useDeviceIntegrity(): IntegrityResult {
|
|
const [result, setResult] = useState<IntegrityResult>(SAFE);
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
checkDeviceIntegrity().then((r) => {
|
|
if (!cancelled) setResult(r);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
return result;
|
|
}
|
|
|
|
// ── B2 앱 무결성 토큰(전송부만) ───────────────────────────────────────────────
|
|
/*
|
|
* Play Integrity(Android)·App Attest(iOS) 토큰은 각 플랫폼 네이티브 모듈로 "발급"한다.
|
|
* 본 앱은 발급기(provider)를 주입받아 "전송"만 담당한다(api.ts가 헤더로 첨부).
|
|
* 서버 검증 엔드포인트는 backend-dev 인계(_workspace/08_mobile_security_hardening.md).
|
|
* provider 미등록 시 getAttestationToken()은 null → 헤더 미첨부(무해).
|
|
*/
|
|
type AttestationProvider = () => Promise<string | null>;
|
|
let attestationProvider: AttestationProvider | null = null;
|
|
|
|
/** 네이티브 무결성 토큰 발급기 등록(추후 Play Integrity/App Attest 모듈 연동 시). */
|
|
export function setAttestationProvider(provider: AttestationProvider | null): void {
|
|
attestationProvider = provider;
|
|
}
|
|
|
|
/** 등록된 발급기로 무결성 토큰 조회. 미등록/실패 시 null(graceful). */
|
|
export async function getAttestationToken(): Promise<string | null> {
|
|
if (!attestationProvider) return null;
|
|
try {
|
|
return await attestationProvider();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|