kintex/mobile/lib/integrity.ts

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;
}
}