Play Integrity API/iOS AppAttest 사용하기 (4. 클라이언트 사이드)
TypeScript/React
2025. 5. 12. 23:29
서론
졸업 프로젝트를 하다 React Native 환경의 클라이언트에서 앱 무결성 검증을 해야 하는 상황이 되었다.
이 기록은 앱 무결성 검증을 하며 헤맨 부분의 기록이다.
본론
React Native의 경우, 여러가지 설정이 복잡하게 들어가있다. 우선 다음과 같은 flag들을 추가해야 한다.
{
"expo": {
"ios": {
"entitlements": {
"com.apple.developer.devicecheck.appattest-environment": "development"
}
}
}
}
해당 코드는 app.json
에 추가되어야 하는 파트이다.
iOS APP을 빌드할 때 AppAttest 환경을 어떤 것을 사용할 것이냐에 대한 필드이다.
const getDeviceId = async() => {
return await DeviceInfo.getUniqueId()
}
const getBundleId = () => {
return DeviceInfo.getBundleId();
}
export const requestChallenge = async () => {
const deviceId = await getDeviceId()
try {
const response = await axios.post<RequestChallengeResponse>(`${apiUrl}/api/integrity/challenge`,
{ deviceId },
{}
)
const data = response.data;
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const expDate = new Date()
expDate.setMinutes(expDate.getMinutes() + data.expiresInMinutes)
await AsyncStorage.setItem('integrityChallenge', data.challenge);
await AsyncStorage.setItem('integrityChallengeExp', expDate.toISOString())
return data.challenge;
} catch (error: any) {
console.error('Failed to get challenge: ' + error.message);
throw error;
}
}
export const verifyDeviceIntegrity = async () => {
let challenge = await AsyncStorage.getItem('integrityChallenge');
const expDate = await AsyncStorage.getItem('integrityChallengeExp');
const deviceId = await getDeviceId()
const platform = Platform.OS;
const bundleId = getBundleId();
if (!challenge || (expDate && new Date(expDate).getTime() < Date.now())) {
challenge = await requestChallenge();
}
try {
const googleCloudProject: number = parseInt(EXPO_PUBLIC_GOOGLE_CLOUD_PROJECT);
let attestation = null
let keyId = null
try {
if (platform == "ios") {
if(!Integrity.isSupported())
throw Error("Integrity is not supported on this device");
attestation = await Integrity.attestKey(challenge);
keyId = await SecureStore.getItemAsync(
SECURE_STORAGE_KEYS.INTEGRITY_KEY_IDENTIFIER,
)
} else if (platform == "android") {
attestation = await Integrity.attestKey(challenge, googleCloudProject);
} else throw Error("Platform not supported");
} catch (error) { throw error }
console.log({
platform,
attestation,
bundleId,
challenge,
deviceId,
keyId
})
const response = await axios.post<VerifyDeviceIntegrityResponse>(`${apiUrl}/api/integrity/verify`, {
platform,
attestation,
bundleId,
challenge,
deviceId,
keyId
}, {
});
const data = response.data
if (response.status !== 200 || !data.isValid) {
throw new Error(`HTTP error! status: ${response.status} / ${data.message} / ${data.details ? JSON.stringify(data.details) : ''}`);
}
if (data.isValid) {
await AsyncStorage.removeItem('integrityChallenge');
await AsyncStorage.removeItem('integrityChallengeExp');
}
return data;
} catch (error: any) {
console.error(`Failed to verify integrity: ${error.message} / ${error.details ? JSON.stringify(error.details) : ''}`);
throw error;
}
}
다음은 Android, iOS 모두 검증하는 클라이언트 사이드 로직이다.
라이브러리는 본인이 직접 수정한 라이브러리를 사용하길 권장한다. expo SDK 53에서도 사용가능함을 검증한 라이브러리이다.
결론
본인이 여러 생고생을 같이 했기 때문에 빨리 이 글을 찾아 광명을 누리길 바란다...