Ver3.0 ๐ก๏ธ ๋ชจ๋ฐ์ผ ๋ณด์: ์๋๋ก์ด๋ ์ฑ ๋ณด์ ์ฝ๋ฉ ๊ฐ์ด๋ ์๋ฒฝ ์ ๋ณตํ๊ธฐ

๐ก๏ธ ๋ชจ๋ฐ์ผ ๋ณด์: ์๋๋ก์ด๋ ์ฑ ๋ณด์ ์ฝ๋ฉ ๊ฐ์ด๋ ์๋ฒฝ ์ ๋ณตํ๊ธฐ
์์ ํ ์ฑ ๊ฐ๋ฐ์ ์ํ ์ค์ ๋ณด์ ์ฝ๋ฉ ๋ ธํ์ฐ
์๋
! ๐ ์ค๋์ ์๋๋ก์ด๋ ์ฑ ๊ฐ๋ฐํ ๋ ์ ๋ ๋นผ๋จน์ผ๋ฉด ์ ๋๋ ๋ณด์ ์ฝ๋ฉ์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ์์ฆ ๋ชจ๋ฐ์ผ ์ฑ ํ๋ ์๋ ์ฌ๋์ด ์ด๋ ์๊ฒ ์ด? ๊ทธ๋งํผ ํด์ปค๋ค๋ ์ฐ๋ฆฌ ์ค๋งํธํฐ์ ๋
ธ๋ฆฌ๊ณ ์๋ค๋ ๊ฑฐ์ง. ๐ฏ
์ค์ ๋ก 2023๋
ํต๊ณ๋ฅผ ๋ณด๋ฉด, ๋ชจ๋ฐ์ผ ์ฑ ๋ณด์ ์ทจ์ฝ์ ์ผ๋ก ์ธํ ํผํด๊ฐ ์ ๋
๋๋น 37% ์ฆ๊ฐํ๋. ๋ฌด์ญ์ง? ํ์ง๋ง ๊ฑฑ์ ๋ง! ์ด ๊ฐ์ด๋๋ฅผ ๋๊น์ง ์ฝ์ผ๋ฉด ๋๋ ๋ณด์ ์ ๋ฌธ๊ฐ์ฒ๋ผ ์์ ํ ์ฑ์ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ. ๐ช
๐ ์ ์๋๋ก์ด๋ ๋ณด์ ์ฝ๋ฉ์ด ์ค์ํ ๊น?
์, ๋จผ์ ๊ธฐ๋ณธ๋ถํฐ ์ง๊ณ ๋์ด๊ฐ์. ์๋๋ก์ด๋๋ ์ ์ธ๊ณ ๋ชจ๋ฐ์ผ OS ์์ฅ์ ์ฝ 72%๋ฅผ ์ฐจ์งํ๊ณ ์์ด. ์ด ๋ง์ ๊ณง ํด์ปค๋ค์ ์ฃผ์ ํ๊ฒ์ด๋ผ๋ ๋ป์ด์ง. ๐ฑ
์ค์ ๋ก ๊ตฌ๊ธ ํ๋ ์ด ์คํ ์ด์์ ๋งค๋
์์ฒ ๊ฐ์ ์
์ฑ ์ฑ์ด ๋ฐ๊ฒฌ๋๊ณ ์๊ณ , ๊ฐ์ธ์ ๋ณด ์ ์ถ, ๊ธ์ต ์ฌ๊ธฐ, ๋์ฌ์จ์ด ๊ณต๊ฒฉ ๋ฑ ๋ค์ํ ๋ณด์ ์ํ์ด ์กด์ฌํด. ๋ ๋ฌด์์ด ๊ฑด ์ด๋ฐ ๊ณต๊ฒฉ์ ๋๋ถ๋ถ์ด ๊ฐ๋ฐ์์ ์์ ์ค์์์ ์์๋๋ค๋ ๊ฑฐ์ผ.
๐ ์๋๋ก์ด๋ ๋ณด์ ์ํ ํต๊ณ (2023๋ ๊ธฐ์ค)
| ์ํ ์ ํ | ๋ฐ์ ๋น์จ | ์ฃผ์ ์์ธ |
|---|---|---|
| ๋ฐ์ดํฐ ์ ์ถ | 42% | ๋ถ์ ์ ํ ๋ฐ์ดํฐ ์ ์ฅ |
| ์ธ์ฆ ์ฐํ | 28% | ์ฝํ ์ธ์ฆ ๋ฉ์ปค๋์ฆ |
| ์ฝ๋ ๋ณ์กฐ | 18% | ๋๋ ํ ๋ฏธ์ ์ฉ |
| ๋คํธ์ํฌ ๊ณต๊ฒฉ | 12% | ์ํธํ ๋ฏธ์ฌ์ฉ |
๐ก ๊ฐ๋ฐ์ TIP
๋ณด์์ ๋์ค์ ์ถ๊ฐํ๋ ๊ฒ ์๋๋ผ ์ฒ์๋ถํฐ ์ค๊ณ์ ํฌํจ์์ผ์ผ ํด. "Security by Design" ์์น์ ๊ธฐ์ตํ์! ๋์ค์ ๋ณด์ ํจ์นํ๋ ๋น์ฉ์ด ์ด๊ธฐ ๊ฐ๋ฐ ๋จ๊ณ์์ ๋ณด์์ ๊ณ ๋ คํ๋ ๊ฒ๋ณด๋ค 10๋ฐฐ ์ด์ ๋ค์ด๊ฐ๋ค๋ ์ฐ๊ตฌ ๊ฒฐ๊ณผ๋ ์์ด.
๐ฏ ์๋๋ก์ด๋ ๋ณด์์ ํต์ฌ ์์น
์๋๋ก์ด๋ ๋ณด์ ์ฝ๋ฉ์ ์์ํ๊ธฐ ์ ์ ์์์ผ ํ ํต์ฌ ์์น๋ค์ด ์์ด. ์ด ์์น๋ค์ ๊ตฌ๊ธ์ด ๊ณต์์ ์ผ๋ก ๊ถ์ฅํ๋ ๊ฒ๋ค์ด๊ณ , ์ค์ ๋ก ๋ง์ ๋ณด์ ์ ๋ฌธ๊ฐ๋ค์ด ๊ฐ์กฐํ๋ ๋ด์ฉ์ด์ผ. ๐
์ฑ์ด ํ์ํ ์ต์ํ์ ๊ถํ๋ง ์์ฒญํด์ผ ํด. ์๋ฅผ ๋ค์ด, ๋จ์ํ ์ฌ์ง์ ๋ณด์ฌ์ฃผ๋ ๊ฐค๋ฌ๋ฆฌ ์ฑ์ด ์ฐ๋ฝ์ฒ ์ ๊ทผ ๊ถํ์ ์์ฒญํ๋ค๋ฉด? ๋น์ฐํ ์์ํ์ง! ๐ค
์๋๋ก์ด๋ 6.0(API ๋ ๋ฒจ 23)๋ถํฐ๋ ๋ฐํ์ ๊ถํ ์์คํ
์ด ๋์
๋์ด์, ์ฌ์ฉ์๊ฐ ์ฑ ์ฌ์ฉ ์ค์ ๊ถํ์ ํ์ฉํ๊ฑฐ๋ ๊ฑฐ๋ถํ ์ ์๊ฒ ๋์ด. ์ด๊ฑธ ์ ๋๋ก ๊ตฌํํ๋ ๊ฒ ์ ๋ง ์ค์ํด.
// AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
// Activity์์ ๋ฐํ์ ๊ถํ ์ฒดํฌ
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// ๊ถํ ์ค๋ช
์ด ํ์ํ ๊ฒฝ์ฐ
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// ์ฌ์ฉ์์๊ฒ ์ ์ด ๊ถํ์ด ํ์ํ์ง ์ค๋ช
showPermissionExplanationDialog();
} else {
// ๊ถํ ์์ฒญ
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
CAMERA_PERMISSION_REQUEST_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// ๊ถํ ์น์ธ๋จ
openCamera();
} else {
// ๊ถํ ๊ฑฐ๋ถ๋จ - ๋์ฒด ๊ธฐ๋ฅ ์ ๊ณต
showAlternativeFeature();
}
}
}
โ ๏ธ ์ฃผ์์ฌํญ
์๋๋ก์ด๋ 11(API ๋ ๋ฒจ 30)๋ถํฐ๋ ์ผ๋ถ ๊ถํ์ด ๋ ์๊ฒฉํด์ก์ด. ํนํ MANAGE_EXTERNAL_STORAGE ๊ฐ์ ๋ฏผ๊ฐํ ๊ถํ์ ๊ตฌ๊ธ ํ๋ ์ด ์คํ ์ด ์ฌ์ฌ์์ ํน๋ณํ ์ฌ์ ๊ฐ ์์ผ๋ฉด ๊ฑฐ๋ถ๋ ์ ์์ผ๋ ์กฐ์ฌํด์ผ ํด!
ํ๋์ ๋ณด์ ์ฅ์น๋ง ๋ฏฟ์ง ๋ง๊ณ , ์ฌ๋ฌ ๊ฒน์ ๋ณด์ ๋ ์ด์ด๋ฅผ ๊ตฌ์ถํ๋ ๊ฑฐ์ผ. ๋ง์น ์ฑ์ ์งํฌ ๋ ์ฑ๋ฒฝ, ํด์, ๋ง๋ฃจ๋ฅผ ๋ชจ๋ ๊ฐ์ถ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ผ. ๐ฐ
์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ์ธ์ฆ์ ํ ๋:
1๋จ๊ณ: ๋น๋ฐ๋ฒํธ ์
๋ ฅ (์ต์ 8์, ๋์๋ฌธ์+์ซ์+ํน์๋ฌธ์ ์กฐํฉ)
2๋จ๊ณ: 2FA(Two-Factor Authentication) - SMS ๋๋ OTP ์ฑ
3๋จ๊ณ: ์์ฒด ์ธ์ฆ (์ง๋ฌธ, ์ผ๊ตด ์ธ์)
4๋จ๊ณ: ๋๋ฐ์ด์ค ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ (๋ฃจํ
ํ์ง)
5๋จ๊ณ: ์ธ์
๊ด๋ฆฌ ๋ฐ ํ์์์
๊ฐ๋ฐ์๋ ์ฌ์ฉ์๊ฐ ๋ณ๋๋ก ์ค์ ํ์ง ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์์ ํ ์ํ์ฌ์ผ ํด. ์๋ฅผ ๋ค์ด, ๋คํธ์ํฌ ํต์ ์ ๊ธฐ๋ณธ์ ์ผ๋ก HTTPS๋ฅผ ์ฌ์ฉํ๊ณ , ๋ฐ์ดํฐ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ํธํ๋์ด ์ ์ฅ๋์ด์ผ ํ๋ ๊ฑฐ์ง. ๐
// res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- ๊ธฐ๋ณธ์ ์ผ๋ก HTTPS๋ง ํ์ฉ -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง HTTP ํ์ฉ (ํ๋ก๋์
์์๋ ์ ๊ฑฐ!) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
<!-- Certificate Pinning ์ค์ -->
<domain-config>
<domain includeSubdomains="true">api.yourapp.com</domain>
<pin-set expiration="2025-12-31">
<pin digest="SHA-256">base64EncodedPublicKeyHash==</pin>
<!-- ๋ฐฑ์
ํ๋ ์ค์ -->
<pin digest="SHA-256">backupKeyHash==</pin>
</pin-set>
</domain-config>
</network-security-config>
// AndroidManifest.xml์ ์ ์ฉ
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
๐พ ์์ ํ ๋ฐ์ดํฐ ์ ์ฅ ๋ฐฉ๋ฒ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ค์ ์ฝ๋ฉ ๊ฐ์ด๋๋ก ๋ค์ด๊ฐ๋ณผ๊น? ๊ฐ์ฅ ๋จผ์ ๋ค๋ฃฐ ์ฃผ์ ๋ ๋ฐ์ดํฐ ์ ์ฅ์ด์ผ. ์ฌ์ฉ์์ ๊ฐ์ธ์ ๋ณด, ์ธ์ฆ ํ ํฐ, ๋ฏผ๊ฐํ ์ค์ ๊ฐ ๋ฑ์ ์ด๋ป๊ฒ ์์ ํ๊ฒ ์ ์ฅํ ์ ์์๊น? ๐๏ธ
๐ซ ์ด๋ ๊ฒ ํ์ง ๋ง์ธ์!
1. SharedPreferences์ ํ๋ฌธ์ผ๋ก ์ ์ฅ
sharedPreferences.edit().putString("password", "myPassword123").apply();2. ํ๋์ฝ๋ฉ๋ ์ํธํ ํค ์ฌ์ฉ
String SECRET_KEY = "mySecretKey123";3. ์ธ๋ถ ์ ์ฅ์์ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ ์ฅ
File file = new File(Environment.getExternalStorageDirectory(), "userdata.txt");4. ๋ก๊ทธ์ ๋ฏผ๊ฐํ ์ ๋ณด ์ถ๋ ฅ
Log.d("Auth", "User token: " + authToken);
์๋๋ก์ด๋์์ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋๋ EncryptedSharedPreferences์ EncryptedFile์ ์ฌ์ฉํด์ผ ํด. ์ด๊ฑด Android Jetpack Security ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํฌํจ๋์ด ์์ด์ ์ฌ์ฉํ๊ธฐ๋ ์ฝ๊ณ ์์ ํด. ๐ก๏ธ
// build.gradle์ ์์กด์ฑ ์ถ๊ฐ
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}
// ์ํธํ๋ SharedPreferences ์์ฑ
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;
public class SecureStorage {
private SharedPreferences encryptedPrefs;
public SecureStorage(Context context) throws Exception {
// MasterKey ์์ฑ (Android Keystore ์ฌ์ฉ)
MasterKey masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
// EncryptedSharedPreferences ์์ฑ
encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
}
// ์์ ํ๊ฒ ๋ฐ์ดํฐ ์ ์ฅ
public void saveAuthToken(String token) {
encryptedPrefs.edit()
.putString("auth_token", token)
.apply();
}
// ์์ ํ๊ฒ ๋ฐ์ดํฐ ์ฝ๊ธฐ
public String getAuthToken() {
return encryptedPrefs.getString("auth_token", null);
}
// ๋ฐ์ดํฐ ์ญ์
public void clearAuthToken() {
encryptedPrefs.edit()
.remove("auth_token")
.apply();
}
}
โจ ์ ์ด ๋ฐฉ๋ฒ์ด ์์ ํ ๊น?
Android Keystore System์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ด์ผ! ์ํธํ ํค๊ฐ ํ๋์จ์ด ๋ณด์ ๋ชจ๋(TEE ๋๋ Secure Element)์ ์ ์ฅ๋์ด์, ๋ฃจํ ๋ ๊ธฐ๊ธฐ์์๋ ํค๋ฅผ ์ถ์ถํ๊ธฐ๊ฐ ๊ฑฐ์ ๋ถ๊ฐ๋ฅํด. ๋ํ AES-256 GCM ๋ชจ๋๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ๊น์ง ๋ณด์ฅํด์ค. ๐
์ด๋ฏธ์ง, ๋ฌธ์, ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ผ ๋ฑ ํฐ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋๋ EncryptedFile์ ์ฌ์ฉํ๋ฉด ๋ผ.
import androidx.security.crypto.EncryptedFile;
public class SecureFileStorage {
public void writeEncryptedFile(Context context, String fileName,
byte[] data) throws Exception {
// MasterKey ์์ฑ
MasterKey masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
// ์ํธํ๋ ํ์ผ ์์ฑ
File file = new File(context.getFilesDir(), fileName);
EncryptedFile encryptedFile = new EncryptedFile.Builder(
context,
file,
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build();
// ๋ฐ์ดํฐ ์ฐ๊ธฐ
try (OutputStream outputStream = encryptedFile.openFileOutput()) {
outputStream.write(data);
}
}
public byte[] readEncryptedFile(Context context,
String fileName) throws Exception {
MasterKey masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
File file = new File(context.getFilesDir(), fileName);
EncryptedFile encryptedFile = new EncryptedFile.Builder(
context,
file,
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build();
// ๋ฐ์ดํฐ ์ฝ๊ธฐ
try (InputStream inputStream = encryptedFile.openFileInput()) {
return inputStream.readAllBytes();
}
}
}
๐ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํธํ๋?
SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ํธํํ๋ ค๋ฉด SQLCipher๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์. Room ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ํธํ๋๊ณ , ์ ์ฒด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ AES-256์ผ๋ก ์ํธํํด์ค. ๋ค๋ง ์ฑ๋ฅ์ด ์ฝ๊ฐ ๋จ์ด์ง ์ ์์ผ๋, ์ ๋ง ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ง ์ํธํํ๋ ๊ฒ ํ๋ช ํด.
๐ ์์ ํ ์ธ์ฆ ๊ตฌํํ๊ธฐ
์ฌ์ฉ์ ์ธ์ฆ์ ์ฑ ๋ณด์์ ํต์ฌ์ด์ผ. ์๋ชป ๊ตฌํํ๋ฉด ์ ์ฒด ์์คํ ์ด ๋ฌด๋์ง ์ ์์ด. ์ฌ๊ธฐ์๋ ํ๋์ ์ด๊ณ ์์ ํ ์ธ์ฆ ๋ฐฉ๋ฒ๋ค์ ์์๋ณผ๊ฒ. ๐ซ
์ง๋ฌธ, ์ผ๊ตด ์ธ์ ๊ฐ์ ์์ฒด ์ธ์ฆ์ ์ฌ์ฉ์ ๊ฒฝํ๋ ์ข๊ณ ๋ณด์๋ ๊ฐ๋ ฅํด. ์๋๋ก์ด๋์์๋ BiometricPrompt API๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ.
import androidx.biometric.BiometricPrompt;
import androidx.biometric.BiometricManager;
public class BiometricAuthHelper {
public void authenticateUser(FragmentActivity activity,
BiometricCallback callback) {
// ์์ฒด ์ธ์ฆ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
BiometricManager biometricManager =
BiometricManager.from(activity);
int canAuthenticate = biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG);
if (canAuthenticate != BiometricManager.BIOMETRIC_SUCCESS) {
// ์์ฒด ์ธ์ฆ ๋ถ๊ฐ๋ฅ - ๋์ฒด ์ธ์ฆ ๋ฐฉ๋ฒ ์ ๊ณต
callback.onAuthenticationError("์์ฒด ์ธ์ฆ์ ์ฌ์ฉํ ์ ์์ต๋๋ค");
return;
}
// Executor ์ค์
Executor executor = ContextCompat.getMainExecutor(activity);
// BiometricPrompt ์ฝ๋ฐฑ
BiometricPrompt.AuthenticationCallback authCallback =
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(
BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
callback.onAuthenticationSuccess();
}
@Override
public void onAuthenticationError(int errorCode,
CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
callback.onAuthenticationError(errString.toString());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
callback.onAuthenticationFailed();
}
};
// BiometricPrompt ์์ฑ
BiometricPrompt biometricPrompt =
new BiometricPrompt(activity, executor, authCallback);
// ํ๋กฌํํธ ์ ๋ณด ์ค์
BiometricPrompt.PromptInfo promptInfo =
new BiometricPrompt.PromptInfo.Builder()
.setTitle("์์ฒด ์ธ์ฆ")
.setSubtitle("์ง๋ฌธ ๋๋ ์ผ๊ตด์ ์ธ์ํด์ฃผ์ธ์")
.setDescription("์ฑ ์ ๊ทผ์ ์ํด ์์ฒด ์ธ์ฆ์ด ํ์ํฉ๋๋ค")
.setNegativeButtonText("์ทจ์")
// ์ํธํ๋ ๋ฐ์ดํฐ ์ ๊ทผ ์
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build();
// ์ธ์ฆ ์์
biometricPrompt.authenticate(promptInfo);
}
// ์ฝ๋ฐฑ ์ธํฐํ์ด์ค
public interface BiometricCallback {
void onAuthenticationSuccess();
void onAuthenticationError(String error);
void onAuthenticationFailed();
}
}
โ ๏ธ ์์ฒด ์ธ์ฆ ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
1. ํด๋ฐฑ ๋ฉ์ปค๋์ฆ ํ์: ์์ฒด ์ธ์ฆ ์คํจ ์ PIN์ด๋ ํจํด ๊ฐ์ ๋์ฒด ์ธ์ฆ ๋ฐฉ๋ฒ์ ์ ๊ณตํด์ผ ํด.
2. ์ธ์ฆ ๊ฐ๋ ์ ํ: BIOMETRIC_STRONG(Class 3), BIOMETRIC_WEAK(Class 2), DEVICE_CREDENTIAL ์ค ์ํฉ์ ๋ง๊ฒ ์ ํํด.
3. ํ์์์ ์ค์ : ์์ฒด ์ธ์ฆ ์ฑ๊ณต ํ์๋ ์ผ์ ์๊ฐ์ด ์ง๋๋ฉด ์ฌ์ธ์ฆ์ ์๊ตฌํด์ผ ํด.
์๋ฒ์ ํต์ ํ ๋๋ JWT(JSON Web Token)๋ฅผ ๋ง์ด ์ฌ์ฉํ์ง? ์ด๊ฒ๋ ์ ๋๋ก ๊ด๋ฆฌํ์ง ์์ผ๋ฉด ๋ณด์ ์ํ์ด ๋ ์ ์์ด.
JWT ํ ํฐ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
โ Access Token์ ์งง์ ๋ง๋ฃ ์๊ฐ ์ค์ (15๋ถ~1์๊ฐ)
โ Refresh Token์ ์ํธํํ์ฌ ์์ ํ๊ฒ ์ ์ฅ
โ ํ ํฐ์ ๋ก๊ทธ์ ์ถ๋ ฅํ์ง ์๊ธฐ
โ HTTPS๋ก๋ง ํ ํฐ ์ ์ก
โ ํ ํฐ ๋ง๋ฃ ์ ์๋ ๊ฐฑ์ ๋ก์ง ๊ตฌํ
โ ๋ก๊ทธ์์ ์ ํ ํฐ ์์ ํ ์ญ์
โ ์๋ฒ ์ธก์์๋ ํ ํฐ ๋ฌดํจํ ๊ฐ๋ฅํ๋๋ก ๊ตฌํ
public class TokenManager {
private static final String ACCESS_TOKEN_KEY = "access_token";
private static final String REFRESH_TOKEN_KEY = "refresh_token";
private static final long TOKEN_EXPIRY_BUFFER = 60000; // 1๋ถ
private SecureStorage secureStorage;
public TokenManager(Context context) throws Exception {
this.secureStorage = new SecureStorage(context);
}
// ํ ํฐ ์ ์ฅ (์ํธํ๋จ)
public void saveTokens(String accessToken, String refreshToken) {
secureStorage.saveAuthToken(ACCESS_TOKEN_KEY, accessToken);
secureStorage.saveAuthToken(REFRESH_TOKEN_KEY, refreshToken);
}
// Access Token ๊ฐ์ ธ์ค๊ธฐ
public String getAccessToken() {
String token = secureStorage.getAuthToken(ACCESS_TOKEN_KEY);
// ํ ํฐ ๋ง๋ฃ ํ์ธ
if (token != null && isTokenExpired(token)) {
// ์๋ ๊ฐฑ์ ์๋
refreshAccessToken();
token = secureStorage.getAuthToken(ACCESS_TOKEN_KEY);
}
return token;
}
// ํ ํฐ ๋ง๋ฃ ํ์ธ
private boolean isTokenExpired(String token) {
try {
// JWT ๋์ฝ๋ฉ (๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ ๊ถ์ฅ: jjwt, java-jwt ๋ฑ)
String[] parts = token.split("\\.");
String payload = new String(
Base64.decode(parts[1], Base64.URL_SAFE));
JSONObject json = new JSONObject(payload);
long exp = json.getLong("exp") * 1000; // ์ด๋ฅผ ๋ฐ๋ฆฌ์ด๋ก ๋ณํ
long now = System.currentTimeMillis();
// ๋ง๋ฃ 1๋ถ ์ ๋ถํฐ ๊ฐฑ์
return (exp - now) < TOKEN_EXPIRY_BUFFER;
} catch (Exception e) {
// ํ์ฑ ์คํจ ์ ๋ง๋ฃ๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผ
return true;
}
}
// Access Token ๊ฐฑ์
private void refreshAccessToken() {
String refreshToken = secureStorage.getAuthToken(REFRESH_TOKEN_KEY);
if (refreshToken == null) {
// Refresh Token ์์ - ์ฌ๋ก๊ทธ์ธ ํ์
notifyReauthenticationRequired();
return;
}
// ์๋ฒ์ ํ ํฐ ๊ฐฑ์ ์์ฒญ
// (์ค์ ๊ตฌํ์ Retrofit ๋ฑ HTTP ํด๋ผ์ด์ธํธ ์ฌ์ฉ)
apiService.refreshToken(refreshToken)
.enqueue(new Callback<TokenResponse>() {
@Override
public void onResponse(Call<TokenResponse> call,
Response<TokenResponse> response) {
if (response.isSuccessful() && response.body() != null) {
TokenResponse tokens = response.body();
saveTokens(tokens.getAccessToken(),
tokens.getRefreshToken());
} else {
// ๊ฐฑ์ ์คํจ - ์ฌ๋ก๊ทธ์ธ ํ์
clearTokens();
notifyReauthenticationRequired();
}
}
@Override
public void onFailure(Call<TokenResponse> call, Throwable t) {
// ๋คํธ์ํฌ ์ค๋ฅ ๋ฑ
Log.e("TokenManager", "Token refresh failed", t);
}
});
}
// ํ ํฐ ์ญ์ (๋ก๊ทธ์์)
public void clearTokens() {
secureStorage.clearAuthToken(ACCESS_TOKEN_KEY);
secureStorage.clearAuthToken(REFRESH_TOKEN_KEY);
}
private void notifyReauthenticationRequired() {
// ์ฌ์ธ์ฆ ํ์ ์ด๋ฒคํธ ๋ฐ์ (EventBus, LiveData ๋ฑ ์ฌ์ฉ)
// ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋
}
}
๐ ๋คํธ์ํฌ ํต์ ๋ณด์
์ฑ๊ณผ ์๋ฒ ๊ฐ์ ํต์ ์ ํด์ปค๋ค์ด ๊ฐ์ฅ ๋ ธ๋ฆฌ๋ ์ง์ ์ด์ผ. ์ค๊ฐ์ ๊ณต๊ฒฉ(MITM), ํจํท ์ค๋ํ, ๋ฐ์ดํฐ ๋ณ์กฐ ๋ฑ ๋ค์ํ ์ํ์ด ์กด์ฌํด. ์ ๋๋ก ๋ณดํธํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐ก๏ธ
"HTTPS ์ฐ๋ฉด ๋๋ ๊ฑฐ ์๋?"๋ผ๊ณ ์๊ฐํ ์ ์๋๋ฐ, ๊ทธ๊ฒ๋ง์ผ๋ก๋ ๋ถ์กฑํด. ๊ณต๊ฒฉ์๊ฐ ๊ฐ์ง ์ธ์ฆ์๋ฅผ ๋ง๋ค์ด์ ์ค๊ฐ์ ๊ณต๊ฒฉ์ ํ ์ ์๊ฑฐ๋ . ์ด๋ฅผ ๋ง๊ธฐ ์ํด Certificate Pinning์ ๊ตฌํํด์ผ ํด.
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
public class SecureHttpClient {
public OkHttpClient createSecureClient() {
// Certificate Pinning ์ค์
CertificatePinner certificatePinner = new CertificatePinner.Builder()
// SHA-256 ํด์๊ฐ (์๋ฒ ์ธ์ฆ์์ ๊ณต๊ฐํค ํด์)
.add("api.yourapp.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
// ๋ฐฑ์
ํ (์ธ์ฆ์ ๊ฐฑ์ ๋๋น)
.add("api.yourapp.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
// ์๋ธ๋๋ฉ์ธ ํฌํจ
.add("*.yourapp.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
// OkHttpClient ์์ฑ
return new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
// ํ์์์ ์ค์
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
// ์ธํฐ์
ํฐ ์ถ๊ฐ (๋ก๊น
, ํค๋ ์ถ๊ฐ ๋ฑ)
.addInterceptor(new AuthInterceptor())
.addInterceptor(new LoggingInterceptor())
.build();
}
// ์ธ์ฆ ํค๋ ์๋ ์ถ๊ฐ ์ธํฐ์
ํฐ
private class AuthInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
// Access Token ๊ฐ์ ธ์ค๊ธฐ
String token = tokenManager.getAccessToken();
if (token != null) {
Request.Builder requestBuilder = original.newBuilder()
.header("Authorization", "Bearer " + token)
.header("User-Agent", "YourApp/1.0 Android")
.method(original.method(), original.body());
Request request = requestBuilder.build();
return chain.proceed(request);
}
return chain.proceed(original);
}
}
// ๋ก๊น
์ธํฐ์
ํฐ (ํ๋ก๋์
์์๋ ๋ฏผ๊ฐํ ์ ๋ณด ์ ์ธ)
private class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (BuildConfig.DEBUG) {
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ๋ก๊น
Log.d("HTTP", "Request: " + request.url());
// โ ๏ธ ์ฃผ์: ํ ํฐ์ด๋ ๋น๋ฐ๋ฒํธ๋ ๋ก๊ทธ์ ์ถ๋ ฅํ์ง ๋ง ๊ฒ!
}
long startTime = System.nanoTime();
Response response = chain.proceed(request);
long endTime = System.nanoTime();
if (BuildConfig.DEBUG) {
Log.d("HTTP", "Response: " + response.code() +
" in " + (endTime - startTime) / 1e6 + "ms");
}
return response;
}
}
}
๐ก Certificate Pinning ํด์๊ฐ ์ป๋ ๋ฐฉ๋ฒ
ํฐ๋ฏธ๋์์ ๋ค์ ๋ช
๋ น์ด๋ฅผ ์คํํ๋ฉด ๋ผ:
openssl s_client -connect api.yourapp.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64๋๋ OkHttp ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ๋ฐํ์์ ์ถ๋ ฅํ ์๋ ์์ด. ๋ค๋ง ํ๋ก๋์ ๋น๋์์๋ ๋ฐ๋์ ์ ๊ฑฐํด์ผ ํด!
HTTPS๋ฅผ ์ฌ์ฉํด๋ ์ถ๊ฐ์ ์ธ ์ํธํ ๋ ์ด์ด๋ฅผ ๋๋ฉด ๋ ์์ ํด. ํนํ ๊ธ์ต ์ฑ์ด๋ ์๋ฃ ์ฑ์ฒ๋ผ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ ๊ฒฝ์ฐ์๋ ํ์์ผ.
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ApiEncryption {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int GCM_IV_LENGTH = 12;
// ์๋ฒ์ ๊ณต์ ํ๋ ์ํธํ ํค (์ค์ ๋ก๋ ํค ๊ตํ ํ๋กํ ์ฝ ์ฌ์ฉ)
private SecretKey secretKey;
public ApiEncryption() throws Exception {
// Android Keystore์์ ํค ๋ก๋ ๋๋ ์์ฑ
this.secretKey = loadOrGenerateKey();
}
// ์์ฒญ ๋ฐ์ดํฐ ์ํธํ
public EncryptedData encryptRequest(String plaintext) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
// IV(Initialization Vector) ์์ฑ
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
GCMParameterSpec parameterSpec =
new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
// ์ํธํ
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
// Base64 ์ธ์ฝ๋ฉํ์ฌ ์ ์ก
String encryptedData = Base64.encodeToString(
ciphertext, Base64.NO_WRAP);
String ivString = Base64.encodeToString(iv, Base64.NO_WRAP);
return new EncryptedData(encryptedData, ivString);
}
// ์๋ต ๋ฐ์ดํฐ ๋ณตํธํ
public String decryptResponse(EncryptedData encryptedData)
throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
byte[] iv = Base64.decode(encryptedData.getIv(), Base64.NO_WRAP);
byte[] ciphertext = Base64.decode(
encryptedData.getData(), Base64.NO_WRAP);
GCMParameterSpec parameterSpec =
new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
// ๋ณตํธํ
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, "UTF-8");
}
// Android Keystore์์ ํค ๋ก๋ ๋๋ ์์ฑ
private SecretKey loadOrGenerateKey() throws Exception {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
String keyAlias = "api_encryption_key";
if (!keyStore.containsAlias(keyAlias)) {
// ํค ์์ฑ
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
KeyGenParameterSpec keyGenParameterSpec =
new KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT |
KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build();
keyGenerator.init(keyGenParameterSpec);
return keyGenerator.generateKey();
} else {
// ๊ธฐ์กด ํค ๋ก๋
return ((KeyStore.SecretKeyEntry) keyStore.getEntry(
keyAlias, null)).getSecretKey();
}
}
// ์ํธํ๋ ๋ฐ์ดํฐ ๋ชจ๋ธ
public static class EncryptedData {
private String data;
private String iv;
public EncryptedData(String data, String iv) {
this.data = data;
this.iv = iv;
}
public String getData() { return data; }
public String getIv() { return iv; }
}
}
โ ๏ธ ํค ๊ด๋ฆฌ ์ฃผ์์ฌํญ
์ํธํ ํค๋ฅผ ์ฑ ์ฝ๋์ ํ๋์ฝ๋ฉํ๋ฉด ์ ๋ ์ ๋ผ! ๋์ปดํ์ผํ๋ฉด ๋ฐ๋ก ๋ ธ์ถ๋๊ฑฐ๋ . ๋ฐ๋์ Android Keystore๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, ์๋ฒ์ ์์ ํ ํค ๊ตํ ํ๋กํ ์ฝ(์: Diffie-Hellman)์ ์ฌ์ฉํด์ผ ํด. ๐
๐ก๏ธ ์ฝ๋ ๋๋ ํ์ ์ฑ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ
์๋ฌด๋ฆฌ ์ฝ๋๋ฅผ ์ ์ง๋ APK ํ์ผ์ ๋์ปดํ์ผํ๋ฉด ์ฝ๋๊ฐ ๋ ธ์ถ๋ ์ ์์ด. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฝ๋ ๋๋ ํ์ ์ฑ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ์ด ํ์ํด. ๐
์๋๋ก์ด๋ ์คํ๋์ค์๋ ๊ธฐ๋ณธ์ ์ผ๋ก R8์ด๋ผ๋ ์ฝ๋ ์ถ์ ๋ฐ ๋๋ ํ ๋๊ตฌ๊ฐ ํฌํจ๋์ด ์์ด. ์ด๊ฑธ ์ ๋๋ก ์ค์ ํ๋ฉด ์ฝ๋ ๋ถ์์ ์์ฒญ ์ด๋ ต๊ฒ ๋ง๋ค ์ ์์ง.
android {
buildTypes {
release {
// ๋๋
ํ ํ์ฑํ
minifyEnabled true
// ์ฝ๋ ์ถ์ ํ์ฑํ
shrinkResources true
// ProGuard ๊ท์น ํ์ผ
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
// ์ถ๊ฐ ์ต์ ํ ์ต์
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
# ๊ธฐ๋ณธ ์ต์ ํ ์ ์ง
-optimizationpasses 5
-dontusemixedcaseclassnames
-verbose
# ๋ชจ๋ ํด๋์ค์ ๋ฉค๋ฒ ๋๋
ํ
-repackageclasses ''
-allowaccessmodification
# ์ค์ํ ํด๋์ค๋ ๋๋
ํํ์ง ์๊ธฐ (ํ์์)
-keep class com.yourapp.model.** { *; }
# Retrofit, Gson ๋ฑ ๋ฆฌํ๋ ์
์ฌ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ณดํธ
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.gson.** { *; }
-keep class retrofit2.** { *; }
# ๋ค์ดํฐ๋ธ ๋ฉ์๋ ๋ณดํธ
-keepclasseswithmembernames class * {
native <methods>;
}
# Enum ๋ณดํธ
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Parcelable ๋ณดํธ
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# ๋ณด์ ๊ด๋ จ ํด๋์ค๋ ๋ ๊ฐ๋ ฅํ๊ฒ ๋๋
ํ
-keep class com.yourapp.security.** { *; }
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}
# ์คํ ํธ๋ ์ด์ค ์ ๋ณด ์ ์ง (ํฌ๋์ ๋ถ์์ฉ)
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
๐ก ๋๋ ํ ๋งคํ ํ์ผ ๋ณด๊ดํ๊ธฐ
๋๋
ํ๋ฅผ ํ๋ฉด mapping.txt ํ์ผ์ด ์์ฑ๋ผ. ์ด ํ์ผ์ ๋๋
ํ ์ ํ์ ํด๋์ค/๋ฉ์๋ ์ด๋ฆ ๋งคํ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ด์, ํฌ๋์ ๋ก๊ทธ๋ฅผ ๋ถ์ํ ๋ ํ์์ผ. ๋ฐ๋์ ๋ฒ์ ๋ณ๋ก ์์ ํ๊ฒ ๋ณด๊ดํด์ผ ํด! ๐
๋ฃจํ ๋ ๊ธฐ๊ธฐ์์๋ ์ฑ์ ๋ณด์์ด ๋ฌด๋ ฅํ๋ ์ ์์ด. ๊ธ์ต ์ฑ์ด๋ ๋ณด์์ด ์ค์ํ ์ฑ์ด๋ผ๋ฉด ๋ฃจํ ํ์ง ๊ธฐ๋ฅ์ ๊ตฌํํด์ผ ํด.
public class RootDetector {
// ์ฌ๋ฌ ๋ฐฉ๋ฒ์ ์กฐํฉํ์ฌ ๋ฃจํ
ํ์ง
public boolean isDeviceRooted() {
return checkRootMethod1() ||
checkRootMethod2() ||
checkRootMethod3() ||
checkRootMethod4();
}
// ๋ฐฉ๋ฒ 1: su ๋ฐ์ด๋๋ฆฌ ์กด์ฌ ํ์ธ
private boolean checkRootMethod1() {
String[] paths = {
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin/su"
};
for (String path : paths) {
if (new File(path).exists()) {
return true;
}
}
return false;
}
// ๋ฐฉ๋ฒ 2: ์ํํ ์ฑ ์ค์น ํ์ธ
private boolean checkRootMethod2() {
String[] packages = {
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.topjohnwu.magisk"
};
PackageManager pm = context.getPackageManager();
for (String packageName : packages) {
try {
pm.getPackageInfo(packageName, 0);
return true; // ํจํค์ง ๋ฐ๊ฒฌ
} catch (PackageManager.NameNotFoundException e) {
// ํจํค์ง ์์
}
}
return false;
}
// ๋ฐฉ๋ฒ 3: ๋น๋ ํ๊ทธ ํ์ธ
private boolean checkRootMethod3() {
String buildTags = android.os.Build.TAGS;
return buildTags != null && buildTags.contains("test-keys");
}
// ๋ฐฉ๋ฒ 4: su ๋ช
๋ น์ด ์คํ ์๋
private boolean checkRootMethod4() {
Process process = null;
try {
process = Runtime.getRuntime().exec(new String[] {
"/system/xbin/which", "su"
});
BufferedReader in = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line = in.readLine();
return line != null;
} catch (Exception e) {
return false;
} finally {
if (process != null) {
process.destroy();
}
}
}
// ๋ฃจํ
๊ฐ์ง ์ ๋์
public void handleRootedDevice() {
// ์ต์
1: ์ฑ ์ข
๋ฃ
// System.exit(0);
// ์ต์
2: ์ ํ๋ ๊ธฐ๋ฅ๋ง ์ ๊ณต
// showLimitedFunctionalityWarning();
// ์ต์
3: ๊ฒฝ๊ณ ๋ง ํ์
new AlertDialog.Builder(context)
.setTitle("๋ณด์ ๊ฒฝ๊ณ ")
.setMessage("๋ฃจํ
๋ ๊ธฐ๊ธฐ์์๋ ์ฑ์ ๋ณด์์ด ๋ณด์ฅ๋์ง ์์ต๋๋ค. " +
"๊ณ์ ์ฌ์ฉํ์๊ฒ ์ต๋๊น?")
.setPositiveButton("๊ณ์", null)
.setNegativeButton("์ข
๋ฃ", (dialog, which) -> {
System.exit(0);
})
.setCancelable(false)
.show();
}
}
์ฑ์ด ๋ณ์กฐ๋์ง ์์๋์ง ํ์ธํ๋ ๊ฒ๋ ์ค์ํด. ํด์ปค๊ฐ APK๋ฅผ ์์ ํด์ ์ ์ฑ ์ฝ๋๋ฅผ ์ฝ์ ํ ์ ์๊ฑฐ๋ .
public class IntegrityChecker {
// ์ฑ ์๋ช
ํ์ธ (๋ฆด๋ฆฌ์ค ๋น๋์ ์ค์ ์๋ช
ํด์๊ฐ์ผ๋ก ๊ต์ฒด)
private static final String EXPECTED_SIGNATURE =
"308204a830820390a003020102020900d..."; // ์ค์ ์๋ช
ํด์
public boolean verifyAppSignature(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(),
PackageManager.GET_SIGNATURES);
for (Signature signature : packageInfo.signatures) {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(signature.toByteArray());
String currentSignature = bytesToHex(md.digest());
if (!EXPECTED_SIGNATURE.equalsIgnoreCase(currentSignature)) {
// ์๋ช
์ด ์ผ์นํ์ง ์์ - ๋ณ์กฐ๋จ!
return false;
}
}
return true;
} catch (Exception e) {
// ๊ฒ์ฆ ์คํจ
return false;
}
}
// ์ค์น ์์ค ํ์ธ
public boolean verifyInstallSource(Context context) {
String installer = context.getPackageManager()
.getInstallerPackageName(context.getPackageName());
// ๊ณต์ ์คํ ์ด์์ ์ค์น๋์๋์ง ํ์ธ
return installer != null && (
installer.equals("com.android.vending") || // Google Play
installer.equals("com.amazon.venezia") || // Amazon Store
installer.equals("com.sec.android.app.samsungapps") // Samsung
);
}
// Google Play Integrity API ์ฌ์ฉ (๊ถ์ฅ)
public void checkPlayIntegrity(Context context,
IntegrityCallback callback) {
// Google Play Integrity API ์ด๊ธฐํ
IntegrityManager integrityManager =
IntegrityManagerFactory.create(context);
// Nonce ์์ฑ (์๋ฒ์์ ๋ฐ์์ค๋ ๊ฒ์ด ๋ ์์ )
String nonce = generateNonce();
// Integrity Token ์์ฒญ
Task<IntegrityTokenResponse> integrityTokenResponse =
integrityManager.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce)
.build());
integrityTokenResponse.addOnSuccessListener(response -> {
String token = response.token();
// ์๋ฒ๋ก ํ ํฐ ์ ์กํ์ฌ ๊ฒ์ฆ
verifyTokenOnServer(token, callback);
});
integrityTokenResponse.addOnFailureListener(e -> {
// ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ์คํจ
callback.onIntegrityCheckFailed();
});
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
private String generateNonce() {
SecureRandom random = new SecureRandom();
byte[] nonce = new byte[32];
random.nextBytes(nonce);
return Base64.encodeToString(nonce, Base64.NO_WRAP);
}
private void verifyTokenOnServer(String token,
IntegrityCallback callback) {
// ์๋ฒ API ํธ์ถํ์ฌ ํ ํฐ ๊ฒ์ฆ
// ์๋ฒ์์ Google Play Integrity API๋ก ํ ํฐ ๊ฒ์ฆ ํ ๊ฒฐ๊ณผ ๋ฐํ
}
public interface IntegrityCallback {
void onIntegrityCheckPassed();
void onIntegrityCheckFailed();
}
}
โจ Google Play Integrity API ์ฅ์
Google Play Integrity API๋ SafetyNet์ ํ์ ๋ฒ์ ์ผ๋ก, ๋ ๊ฐ๋ ฅํ๊ณ ์ ํํ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ์ ์ ๊ณตํด. ์ฑ์ด ์ ํ์ธ์ง, ๊ธฐ๊ธฐ๊ฐ ์์ ํ์ง, Play Store์์ ์ค์น๋์๋์ง ๋ฑ์ ์ข ํฉ์ ์ผ๋ก ํ๋จํด์ค. ๊ธ์ต ์ฑ์ด๋ ๋ณด์์ด ์ค์ํ ์ฑ์ด๋ผ๋ฉด ํ์๋ก ๊ตฌํํ๋ ๊ฒ ์ข์! ๐ฏ
๐ ์์ ํ ์ปดํฌ๋ํธ ์ฌ์ฉํ๊ธฐ
์๋๋ก์ด๋์ 4๋ ์ปดํฌ๋ํธ(Activity, Service, BroadcastReceiver, ContentProvider)๋ฅผ ์๋ชป ์ฌ์ฉํ๋ฉด ๋ณด์ ์ทจ์ฝ์ ์ด ์๊ธธ ์ ์์ด. ๊ฐ ์ปดํฌ๋ํธ๋ฅผ ์์ ํ๊ฒ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐งฉ
Activity๋ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ๋ด๋นํ๋ ์ปดํฌ๋ํธ์ผ. ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋ค๋ฃจ๋ Activity๋ ํน๋ณํ ์ฃผ์ํด์ผ ํด.
<!-- AndroidManifest.xml -->
<activity
android:name=".ui.PaymentActivity"
android:exported="false"
android:excludeFromRecents="true"
android:noHistory="true"
android:windowSoftInputMode="stateAlwaysHidden">
<!-- Intent Filter๊ฐ ์์ผ๋ฉด exported="true"๊ฐ ๊ธฐ๋ณธ๊ฐ -->
<!-- ์ธ๋ถ ์ฑ์ ์ ๊ทผ์ด ํ์ํ ๊ฒฝ์ฐ์๋ง exported="true" ์ค์ -->
</activity>
<!-- ์ธ๋ถ์์ ์ ๊ทผ ๊ฐ๋ฅํ Activity๋ ๊ถํ ์ค์ -->
<activity
android:name=".ui.ShareActivity"
android:exported="true"
android:permission="com.yourapp.permission.SHARE_DATA">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
public class SecureActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ์คํฌ๋ฆฐ์ท ๋ฐฉ์ง
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE);
// ์ต๊ทผ ์ฑ ๋ชฉ๋ก์์ ์จ๊ธฐ๊ธฐ
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setExcludeFromRecents(true);
}
setContentView(R.layout.activity_secure);
}
@Override
protected void onPause() {
super.onPause();
// ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๊ฐ ๋ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ง์ฐ๊ธฐ
clearSensitiveData();
}
// Intent๋ก ๋ฐ์ดํฐ ์ ๋ฌ ์ ์ฃผ์
public void startSecureActivity(String sensitiveData) {
Intent intent = new Intent(this, TargetActivity.class);
// โ ๋์ ์: Intent Extra๋ก ์ง์ ์ ๋ฌ
// intent.putExtra("password", sensitiveData);
// โ
์ข์ ์: ์์ ์ ์ฅ์ ์ฌ์ฉ
String token = SecureDataStore.storeTempData(sensitiveData);
intent.putExtra("data_token", token);
startActivity(intent);
}
private void clearSensitiveData() {
// EditText ๋ด์ฉ ์ง์ฐ๊ธฐ
EditText passwordField = findViewById(R.id.password_field);
if (passwordField != null) {
passwordField.setText("");
}
// ๋ฉ๋ชจ๋ฆฌ์์ ๋ฏผ๊ฐํ ๋ณ์ ์ง์ฐ๊ธฐ
// (Java๋ GC๊ฐ ๊ด๋ฆฌํ์ง๋ง, ๋ช
์์ ์ผ๋ก null ํ ๋น)
sensitiveData = null;
}
}
BroadcastReceiver๋ ์์คํ ์ด๋ ๋ค๋ฅธ ์ฑ์ ์ด๋ฒคํธ๋ฅผ ๋ฐ๋ ์ปดํฌ๋ํธ์ผ. ์๋ชป ์ค์ ํ๋ฉด ์ ์์ ์ธ ์ฑ์ด ๊ฐ์ง ๋ธ๋ก๋์บ์คํธ๋ฅผ ๋ณด๋ผ ์ ์์ด.
<!-- AndroidManifest.xml -->
<!-- ๋ด๋ถ ๋ธ๋ก๋์บ์คํธ๋ง ๋ฐ๊ธฐ -->
<receiver
android:name=".receiver.SecureReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.yourapp.ACTION_SECURE_EVENT" />
</intent-filter>
</receiver>
<!-- ์ธ๋ถ ๋ธ๋ก๋์บ์คํธ๋ฅผ ๋ฐ์์ผ ํ๋ ๊ฒฝ์ฐ ๊ถํ ์ค์ -->
<receiver
android:name=".receiver.PublicReceiver"
android:exported="true"
android:permission="com.yourapp.permission.SEND_BROADCAST">
<intent-filter>
<action android:name="com.yourapp.ACTION_PUBLIC_EVENT" />
</intent-filter>
</receiver>
<!-- ์ปค์คํ
๊ถํ ์ ์ -->
<permission
android:name="com.yourapp.permission.SEND_BROADCAST"
android:protectionLevel="signature" />
public class SecureBroadcastHelper {
// ๋ด๋ถ ๋ธ๋ก๋์บ์คํธ ์ ์ก (LocalBroadcastManager ์ฌ์ฉ)
public void sendLocalBroadcast(Context context, String action,
Bundle data) {
Intent intent = new Intent(action);
if (data != null) {
intent.putExtras(data);
}
// LocalBroadcastManager๋ ์ฑ ๋ด๋ถ์์๋ง ์ ๋ฌ๋จ
LocalBroadcastManager.getInstance(context)
.sendBroadcast(intent);
}
// ๊ถํ์ด ์๋ ์ฑ์๊ฒ๋ง ๋ธ๋ก๋์บ์คํธ ์ ์ก
public void sendProtectedBroadcast(Context context, String action,
Bundle data) {
Intent intent = new Intent(action);
if (data != null) {
intent.putExtras(data);
}
// ํน์ ๊ถํ์ ๊ฐ์ง ์ฑ๋ง ๋ฐ์ ์ ์์
context.sendBroadcast(intent,
"com.yourapp.permission.RECEIVE_BROADCAST");
}
// ํน์ ํจํค์ง์๊ฒ๋ง ๋ธ๋ก๋์บ์คํธ ์ ์ก
public void sendTargetedBroadcast(Context context, String action,
String targetPackage, Bundle data) {
Intent intent = new Intent(action);
intent.setPackage(targetPackage); // ํจํค์ง ์ง์
if (data != null) {
intent.putExtras(data);
}
context.sendBroadcast(intent);
}
}
// BroadcastReceiver ๊ตฌํ
public class SecureReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// ๋ฐ์ ์ ๊ฒ์ฆ
if (!verifyBroadcastSender(context, intent)) {
Log.w("SecureReceiver", "Unauthorized broadcast received");
return;
}
// ์์ ํ๊ฒ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
String action = intent.getAction();
if ("com.yourapp.ACTION_SECURE_EVENT".equals(action)) {
handleSecureEvent(intent);
}
}
private boolean verifyBroadcastSender(Context context, Intent intent) {
// ๋ฐ์ ์ ํจํค์ง ํ์ธ
String senderPackage = intent.getStringExtra("sender_package");
// ์ ๋ขฐํ ์ ์๋ ํจํค์ง ๋ชฉ๋ก
Set<String> trustedPackages = new HashSet<>(Arrays.asList(
"com.yourapp",
"com.yourapp.debug"
));
return trustedPackages.contains(senderPackage);
}
private void handleSecureEvent(Intent intent) {
// ์ด๋ฒคํธ ์ฒ๋ฆฌ
}
}
ContentProvider๋ ์ฑ ๊ฐ ๋ฐ์ดํฐ ๊ณต์ ๋ฅผ ์ํ ์ปดํฌ๋ํธ์ผ. ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ํ์ผ์ ๋ค๋ฅธ ์ฑ๊ณผ ๊ณต์ ํ ๋ ์ฌ์ฉํ๋๋ฐ, ๋ณด์ ์ค์ ์ด ์ ๋ง ์ค์ํด.
<!-- AndroidManifest.xml -->
<provider
android:name=".provider.SecureContentProvider"
android:authorities="com.yourapp.provider"
android:exported="false"
android:grantUriPermissions="true"
android:readPermission="com.yourapp.permission.READ_DATA"
android:writePermission="com.yourapp.permission.WRITE_DATA">
<!-- ํน์ ๊ฒฝ๋ก๋ง ๊ณต์ -->
<path-permission
android:path="/public/*"
android:readPermission="android.permission.INTERNET" />
</provider>
<!-- ๊ถํ ์ ์ -->
<permission
android:name="com.yourapp.permission.READ_DATA"
android:protectionLevel="signature" />
<permission
android:name="com.yourapp.permission.WRITE_DATA"
android:protectionLevel="signature" />
public class SecureContentProvider extends ContentProvider {
private static final String AUTHORITY = "com.yourapp.provider";
private static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
@Override
public boolean onCreate() {
// ์ด๊ธฐํ
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// ํธ์ถ์ ๊ฒ์ฆ
if (!verifyCallerPermission()) {
throw new SecurityException("Unauthorized access");
}
// SQL Injection ๋ฐฉ์ง
String sanitizedSelection = sanitizeSelection(selection);
String[] sanitizedArgs = sanitizeSelectionArgs(selectionArgs);
// ์ฟผ๋ฆฌ ์คํ
return database.query(getTableName(uri), projection,
sanitizedSelection, sanitizedArgs, null, null, sortOrder);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (!verifyCallerPermission()) {
throw new SecurityException("Unauthorized access");
}
// ์
๋ ฅ๊ฐ ๊ฒ์ฆ
if (!validateContentValues(values)) {
throw new IllegalArgumentException("Invalid data");
}
long id = database.insert(getTableName(uri), null, values);
if (id > 0) {
Uri newUri = ContentUris.withAppendedId(BASE_URI, id);
getContext().getContentResolver().notifyChange(newUri, null);
return newUri;
}
return null;
}
private boolean verifyCallerPermission() {
// ํธ์ถ์์ UID ํ์ธ
int callingUid = Binder.getCallingUid();
String callingPackage = getContext().getPackageManager()
.getNameForUid(callingUid);
// ์ ๋ขฐํ ์ ์๋ ํจํค์ง์ธ์ง ํ์ธ
return isPackageTrusted(callingPackage);
}
private String sanitizeSelection(String selection) {
if (selection == null) return null;
// SQL Injection ๋ฐฉ์ง: ์ํํ ํค์๋ ์ ๊ฑฐ
String[] dangerousKeywords = {
"DROP", "DELETE", "INSERT", "UPDATE",
"EXEC", "EXECUTE", "--", "/*", "*/"
};
String sanitized = selection;
for (String keyword : dangerousKeywords) {
sanitized = sanitized.replaceAll(
"(?i)" + keyword, "");
}
return sanitized;
}
private boolean validateContentValues(ContentValues values) {
// ํ์ ํ๋ ํ์ธ
if (!values.containsKey("required_field")) {
return false;
}
// ๋ฐ์ดํฐ ํ์
๋ฐ ๋ฒ์ ๊ฒ์ฆ
String name = values.getAsString("name");
if (name == null || name.length() > 100) {
return false;
}
return true;
}
}
โ ๏ธ SQL Injection ์ฃผ์
ContentProvider์์ ๊ฐ์ฅ ํํ ๋ณด์ ์ทจ์ฝ์ ์ด SQL Injection์ด์ผ. ์ฌ์ฉ์ ์
๋ ฅ์ ์ง์ SQL ์ฟผ๋ฆฌ์ ๋ฃ์ง ๋ง๊ณ , ๋ฐ๋์ ํ๋ผ๋ฏธํฐํ๋ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ผ ํด. selectionArgs ๋งค๊ฐ๋ณ์๋ฅผ ํ์ฉํ๋ฉด ์์ ํ๊ฒ ์ฟผ๋ฆฌ๋ฅผ ์คํํ ์ ์์ด! ๐
๐งช ๋ณด์ ํ ์คํธ์ ์ทจ์ฝ์ ์ ๊ฒ
์๋ฌด๋ฆฌ ๋ณด์ ์ฝ๋ฉ์ ์ํด๋ ํ ์คํธ ์์ด๋ ์๋ฒฝํ ์ ์์ด. ์ค์ ๋ก ์ฑ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ๋ฐ๋์ ๋ณด์ ํ ์คํธ๋ฅผ ํด์ผ ํด. ์ด๋ค ๋๊ตฌ๋ค์ ์ฌ์ฉํ ์ ์๋์ง ์์๋ณด์! ๐ฌ
์ฝ๋๋ฅผ ์คํํ์ง ์๊ณ ๋ถ์ํ๋ ๋๊ตฌ๋ค์ด์ผ. ๊ฐ๋ฐ ๋จ๊ณ์์ ์ทจ์ฝ์ ์ ๋ฏธ๋ฆฌ ์ฐพ์ ์ ์์ด.
์ถ์ฒ ์ ์ ๋ถ์ ๋๊ตฌ
1. Android Lint
์๋๋ก์ด๋ ์คํ๋์ค์ ๋ด์ฅ๋ ๊ธฐ๋ณธ ๋๊ตฌ. ๋ณด์ ์ด์๋ฟ๋ง ์๋๋ผ ์ฑ๋ฅ, ์ ๊ทผ์ฑ ๋ฑ ๋ค์ํ ๋ฌธ์ ๋ฅผ ์ฐพ์์ค.
./gradlew lint
2. SpotBugs (FindBugs ํ์)
์๋ฐ ๋ฐ์ดํธ์ฝ๋๋ฅผ ๋ถ์ํด์ ๋ฒ๊ทธ์ ๋ณด์ ์ทจ์ฝ์ ์ ์ฐพ์์ค.
./gradlew spotbugsRelease
3. MobSF (Mobile Security Framework)
์คํ์์ค ๋ชจ๋ฐ์ผ ์ฑ ๋ณด์ ๋ถ์ ํ๋ซํผ. APK ํ์ผ์ ์
๋ก๋ํ๋ฉด ์๋์ผ๋ก ๋ถ์ํด์ค.
์น ๊ธฐ๋ฐ ์ธํฐํ์ด์ค๋ก ์ฌ์ฉํ๊ธฐ ์ฌ์.
4. QARK (Quick Android Review Kit)
LinkedIn์์ ๋ง๋ ์คํ์์ค ๋๊ตฌ. ์๋๋ก์ด๋ ์ฑ์ ๋ณด์ ์ทจ์ฝ์ ์ ์๋์ผ๋ก ์ฐพ์์ค.
5. AndroBugs Framework
Python ๊ธฐ๋ฐ์ ์๋๋ก์ด๋ ์ทจ์ฝ์ ์ค์บ๋. ์์ธํ ๋ณด๊ณ ์๋ฅผ ์ ๊ณตํด.
plugins {
id 'com.android.application'
id 'com.github.spotbugs' version '5.0.13'
}
// SpotBugs ์ค์
spotbugs {
ignoreFailures = false
effort = 'max'
reportLevel = 'low'
excludeFilter = file("$rootProject.projectDir/config/spotbugs-exclude.xml")
}
// Lint ์ค์
android {
lintOptions {
// ๋ณด์ ๊ด๋ จ ์ด์๋ฅผ ์๋ฌ๋ก ์ฒ๋ฆฌ
error 'HardcodedText', 'InvalidPackage'
warning 'MissingTranslation'
// ์์ธํ ๋ณด๊ณ ์ ์์ฑ
abortOnError true
checkReleaseBuilds true
// HTML ๋ณด๊ณ ์ ์์ฑ
htmlReport true
htmlOutput file("$buildDir/reports/lint-results.html")
}
}
์ค์ ๋ก ์ฑ์ ์คํํ๋ฉด์ ๋ณด์ ์ทจ์ฝ์ ์ ์ฐพ๋ ๋๊ตฌ๋ค์ด์ผ. ๋ฐํ์ ํ๊ฒฝ์์๋ง ๋ฐ๊ฒฌํ ์ ์๋ ๋ฌธ์ ๋ค์ ์ฐพ์์ค.
์ถ์ฒ ๋์ ๋ถ์ ๋๊ตฌ
1. Frida
๋์ ๊ณ์ธก ๋๊ตฌ. ์คํ ์ค์ธ ์ฑ์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์กฐ์ํ๊ณ ํจ์ ํธ์ถ์ ๊ฐ๋ก์ฑ ์ ์์ด. ๋ณด์ ํ
์คํธ์ ํ์์ ์ธ ๋๊ตฌ์ผ.
2. Burp Suite
์น ์ ํ๋ฆฌ์ผ์ด์
๋ณด์ ํ
์คํธ ๋๊ตฌ์ง๋ง, ๋ชจ๋ฐ์ผ ์ฑ์ ๋คํธ์ํฌ ํธ๋ํฝ ๋ถ์์๋ ์ฌ์ฉ๋ผ. ํ๋ก์ ์๋ฒ๋ก ๋์ํด์ ์ฑ๊ณผ ์๋ฒ ๊ฐ์ ํต์ ์ ๊ฐ๋ก์ฑ๊ณ ๋ถ์ํ ์ ์์ด.
3. OWASP ZAP
์คํ์์ค ๋ณด์ ํ
์คํธ ๋๊ตฌ. Burp Suite์ ๋ฌด๋ฃ ๋์์ผ๋ก ๋ง์ด ์ฌ์ฉ๋ผ.
4. Drozer
์๋๋ก์ด๋ ์ฑ์ ๊ณต๊ฒฉ ํ๋ฉด์ ํ๊ฐํ๋ ๋๊ตฌ. ์ปดํฌ๋ํธ ๋
ธ์ถ, ๊ถํ ๋ฌธ์ ๋ฑ์ ์ฐพ์์ค.
// Frida ์คํฌ๋ฆฝํธ (JavaScript)
// ์ํธํ ํจ์ ํํนํ์ฌ ํ๋ฌธ ๋ฐ์ดํฐ ํ์ธ
Java.perform(function() {
// AES ์ํธํ ํด๋์ค ํํน
var Cipher = Java.use('javax.crypto.Cipher');
Cipher.doFinal.overload('[B').implementation = function(data) {
console.log('[*] Cipher.doFinal called');
console.log('[*] Input data: ' + bytesToHex(data));
var result = this.doFinal(data);
console.log('[*] Output data: ' + bytesToHex(result));
return result;
};
// SharedPreferences ํํน
var SharedPreferences = Java.use(
'android.content.SharedPreferences$Editor');
SharedPreferences.putString.implementation = function(key, value) {
console.log('[*] SharedPreferences.putString');
console.log('[*] Key: ' + key);
console.log('[*] Value: ' + value);
return this.putString(key, value);
};
// ๋คํธ์ํฌ ์์ฒญ ํํน
var OkHttpClient = Java.use('okhttp3.OkHttpClient');
var Request = Java.use('okhttp3.Request');
OkHttpClient.newCall.implementation = function(request) {
console.log('[*] HTTP Request');
console.log('[*] URL: ' + request.url());
console.log('[*] Method: ' + request.method());
var headers = request.headers();
for (var i = 0; i < headers.size(); i++) {
console.log('[*] Header: ' + headers.name(i) +
' = ' + headers.value(i));
}
return this.newCall(request);
};
function bytesToHex(bytes) {
var hex = '';
for (var i = 0; i < bytes.length; i++) {
hex += ('0' + (bytes[i] & 0xFF).toString(16)).slice(-2);
}
return hex;
}
console.log('[*] Frida script loaded');
});
๐ก Frida ์ฌ์ฉ๋ฒ
1. ๋ฃจํ
๋ ์๋๋ก์ด๋ ๊ธฐ๊ธฐ ๋๋ ์๋ฎฌ๋ ์ดํฐ ์ค๋น
2. Frida ์๋ฒ๋ฅผ ๊ธฐ๊ธฐ์ ์ค์น: adb push frida-server /data/local/tmp/
3. Frida ์๋ฒ ์คํ: adb shell "/data/local/tmp/frida-server &"
4. PC์์ Frida ์คํฌ๋ฆฝํธ ์คํ: frida -U -f com.yourapp -l script.js
์ด๋ ๊ฒ ํ๋ฉด ์ฑ์ ๋ด๋ถ ๋์์ ์ค์๊ฐ์ผ๋ก ๊ด์ฐฐํ๊ณ ์กฐ์ํ ์ ์์ด!
์ฑ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ๋ฐ๋์ ํ์ธํด์ผ ํ ๋ณด์ ํญ๋ชฉ๋ค์ ์ ๋ฆฌํด๋ดค์ด. ์ด ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ฐ๋ผ๊ฐ๋ฉด ๋๋ถ๋ถ์ ๋ณด์ ๋ฌธ์ ๋ฅผ ์๋ฐฉํ ์ ์์ด! โ
๐ ์๋๋ก์ด๋ ์ฑ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
๋ฐ์ดํฐ ์ ์ฅ
โ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ EncryptedSharedPreferences ์ฌ์ฉ
โ ํ์ผ ์ํธํ๋ EncryptedFile ์ฌ์ฉ
โ ์ธ๋ถ ์ ์ฅ์์ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ ์ฅ ๊ธ์ง
โ ๋ก๊ทธ์ ๋ฏผ๊ฐํ ์ ๋ณด ์ถ๋ ฅ ๊ธ์ง
โ ํ๋์ฝ๋ฉ๋ ์ํธํ ํค ์ ๊ฑฐ
โ Android Keystore ์ฌ์ฉ
๋คํธ์ํฌ ํต์
โ HTTPS๋ง ์ฌ์ฉ (HTTP ์ฐจ๋จ)
โ Certificate Pinning ๊ตฌํ
โ ๋คํธ์ํฌ ๋ณด์ ์ค์ ํ์ผ ์ ์ฉ
โ API ์์ฒญ/์๋ต ์ถ๊ฐ ์ํธํ (ํ์์)
โ ํ์์์ ์ค์
โ ์ฌ์๋ ๋ก์ง ๊ตฌํ
์ธ์ฆ ๋ฐ ๊ถํ
โ ์์ฒด ์ธ์ฆ ๊ตฌํ
โ JWT ํ ํฐ ์์ ํ๊ฒ ๊ด๋ฆฌ
โ ํ ํฐ ์๋ ๊ฐฑ์ ๋ก์ง
โ ์ต์ ๊ถํ ์์น ์ค์
โ ๋ฐํ์ ๊ถํ ์์ฒญ ๊ตฌํ
โ ๊ถํ ์ค๋ช
์ ๊ณต
์ปดํฌ๋ํธ ๋ณด์
โ ๋ถํ์ํ ์ปดํฌ๋ํธ exported="false" ์ค์
โ Intent Filter ์ต์ํ
โ ์ปค์คํ
๊ถํ ์ ์ ๋ฐ ์ฌ์ฉ
โ ContentProvider SQL Injection ๋ฐฉ์ง
โ BroadcastReceiver ๋ฐ์ ์ ๊ฒ์ฆ
โ ๋ฏผ๊ฐํ Activity ์คํฌ๋ฆฐ์ท ๋ฐฉ์ง
์ฝ๋ ๋ณด์
โ ProGuard/R8 ๋๋
ํ ํ์ฑํ
โ ๋ฃจํ
ํ์ง ๊ตฌํ
โ ์ฑ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ
โ Google Play Integrity API ์ฌ์ฉ
โ ๋๋ฒ๊ทธ ์ฝ๋ ์ ๊ฑฐ
โ ๋ฐฑ๋์ด ์ ๊ฑฐ
ํ
์คํธ
โ ์ ์ ๋ถ์ ๋๊ตฌ ์คํ (Lint, SpotBugs)
โ ๋์ ๋ถ์ ๋๊ตฌ ์คํ (Frida, Burp Suite)
โ ์นจํฌ ํ
์คํธ ์ํ
โ ๋ณด์ ์ ๋ฌธ๊ฐ ๋ฆฌ๋ทฐ
โ ๋ฒ ํ ํ
์คํธ ์งํ
๐ ๋ณด์ ์ฝ๋ฉ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ๋ค์ ์ค์ ๊ฐ๋ฐ์ ์ด๋ป๊ฒ ์ ์ฉํ ์ ์์๊น? ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ถ์ฒํ๋ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ์ ๋ฆฌํด๋ดค์ด! ๐
"๋์ค์ ๋ณด์ ์ถ๊ฐํ๋ฉด ๋์ง"๋ผ๋ ์๊ฐ์ ์ ๋ง ์ํํด. ๋ณด์์ ์ํคํ ์ฒ ์ค๊ณ ๋จ๊ณ๋ถํฐ ๊ณ ๋ คํด์ผ ํด. ์๋ฅผ ๋ค์ด:
์ค๊ณ ๋จ๊ณ์์ ๊ฒฐ์ ํ ์ฌํญ๋ค:
โข ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ์ํธํํ ๊ฒ์ธ๊ฐ?
โข ์ธ์ฆ ๋ฐฉ์์ ๋ฌด์์ ์ฌ์ฉํ ๊ฒ์ธ๊ฐ?
โข ๋คํธ์ํฌ ํต์ ์ ์ด๋ป๊ฒ ๋ณดํธํ ๊ฒ์ธ๊ฐ?
โข ๋ก๊ทธ๋ ์ด๋ป๊ฒ ๊ด๋ฆฌํ ๊ฒ์ธ๊ฐ?
โข ์๋ฌ ์ฒ๋ฆฌ๋ ์ด๋ป๊ฒ ํ ๊ฒ์ธ๊ฐ?
โข ๋ฐฑ์
๋ฐ ๋ณต๊ตฌ ์ ๋ต์?
์ฌ์ฉ์ ์ ๋ ฅ, ๋คํธ์ํฌ ์๋ต, Intent ๋ฐ์ดํฐ ๋ฑ ์ธ๋ถ์์ ๋ค์ด์ค๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ ์ ๋ขฐํ์ง ๋ง๊ณ ๊ฒ์ฆํด์ผ ํด.
public class InputValidator {
// ์ด๋ฉ์ผ ๊ฒ์ฆ
public static boolean isValidEmail(String email) {
if (email == null || email.isEmpty()) {
return false;
}
String emailPattern =
"^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
return email.matches(emailPattern);
}
// ๋น๋ฐ๋ฒํธ ๊ฐ๋ ๊ฒ์ฆ
public static boolean isStrongPassword(String password) {
if (password == null || password.length() < 8) {
return false;
}
// ๋๋ฌธ์, ์๋ฌธ์, ์ซ์, ํน์๋ฌธ์ ํฌํจ ์ฌ๋ถ ํ์ธ
boolean hasUpperCase = password.matches(".*[A-Z].*");
boolean hasLowerCase = password.matches(".*[a-z].*");
boolean hasDigit = password.matches(".*\\d.*");
boolean hasSpecialChar = password.matches(".*[!@#$%^&*()].*");
return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar;
}
// ์ ํ๋ฒํธ ๊ฒ์ฆ
public static boolean isValidPhoneNumber(String phone) {
if (phone == null || phone.isEmpty()) {
return false;
}
// ํ๊ตญ ์ ํ๋ฒํธ ํ์
String phonePattern = "^01[0-9]-?[0-9]{3,4}-?[0-9]{4}$";
return phone.matches(phonePattern);
}
// URL ๊ฒ์ฆ
public static boolean isValidUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
try {
URL urlObj = new URL(url);
// HTTPS๋ง ํ์ฉ
if (!"https".equals(urlObj.getProtocol())) {
return false;
}
// ํ์ดํธ๋ฆฌ์คํธ ๋๋ฉ์ธ ํ์ธ
String host = urlObj.getHost();
return isWhitelistedDomain(host);
} catch (MalformedURLException e) {
return false;
}
}
// ํ์ผ ์ด๋ฆ ๊ฒ์ฆ (๊ฒฝ๋ก ํ์ ๊ณต๊ฒฉ ๋ฐฉ์ง)
public static boolean isValidFileName(String fileName) {
if (fileName == null || fileName.isEmpty()) {
return false;
}
// ์ํํ ๋ฌธ์ ํ์ธ
String[] dangerousChars = {"..", "/", "\\", ":", "*", "?",
"\"", "<", ">", "|"};
for (String dangerous : dangerousChars) {
if (fileName.contains(dangerous)) {
return false;
}
}
// ํ์ผ ํ์ฅ์ ํ์ดํธ๋ฆฌ์คํธ
String[] allowedExtensions = {".jpg", ".png", ".pdf", ".txt"};
boolean hasAllowedExtension = false;
for (String ext : allowedExtensions) {
if (fileName.toLowerCase().endsWith(ext)) {
hasAllowedExtension = true;
break;
}
}
return hasAllowedExtension;
}
// HTML/JavaScript ์ธ์ ์
๋ฐฉ์ง
public static String sanitizeHtml(String input) {
if (input == null) {
return "";
}
return input.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """)
.replaceAll("'", "'")
.replaceAll("/", "/");
}
private static boolean isWhitelistedDomain(String host) {
String[] whitelist = {
"api.yourapp.com",
"cdn.yourapp.com",
"www.yourapp.com"
};
for (String domain : whitelist) {
if (host.equals(domain) || host.endsWith("." + domain)) {
return true;
}
}
return false;
}
}
์๋ฌ ๋ฉ์์ง์ ๋๋ฌด ๋ง์ ์ ๋ณด๋ฅผ ๋ด์ผ๋ฉด ๊ณต๊ฒฉ์์๊ฒ ํํธ๋ฅผ ์ค ์ ์์ด. ์ฌ์ฉ์์๊ฒ๋ ์น์ ํ๊ฒ, ํ์ง๋ง ์์คํ ์ ๋ณด๋ ๋ ธ์ถํ์ง ์๋๋ก ํด์ผ ํด.
public class SecureErrorHandler {
private static final String TAG = "SecureErrorHandler";
// โ ๋์ ์: ๋๋ฌด ์์ธํ ์๋ฌ ๋ฉ์์ง
public void badErrorHandling(Exception e) {
Toast.makeText(context,
"Error: " + e.getMessage() + "\n" +
"Stack trace: " + e.getStackTrace()[0],
Toast.LENGTH_LONG).show();
}
// โ
์ข์ ์: ์ผ๋ฐ์ ์ธ ์๋ฌ ๋ฉ์์ง
public void goodErrorHandling(Exception e) {
// ์ฌ์ฉ์์๊ฒ๋ ์ผ๋ฐ์ ์ธ ๋ฉ์์ง
Toast.makeText(context,
"์์
์ ์๋ฃํ ์ ์์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.",
Toast.LENGTH_SHORT).show();
// ๊ฐ๋ฐ์์ฉ ๋ก๊ทธ (ํ๋ก๋์
์์๋ ์ ๊ฑฐ ๋๋ ์๋ฒ๋ก ์ ์ก)
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error occurred", e);
} else {
// ํ๋ก๋์
: ์๋ฌ ๋ก๊ทธ๋ฅผ ์๋ฒ๋ก ์ ์ก
sendErrorToServer(e);
}
}
// ๋คํธ์ํฌ ์๋ฌ ์ฒ๋ฆฌ
public void handleNetworkError(Throwable error) {
String userMessage;
if (error instanceof UnknownHostException) {
userMessage = "์ธํฐ๋ท ์ฐ๊ฒฐ์ ํ์ธํด์ฃผ์ธ์.";
} else if (error instanceof SocketTimeoutException) {
userMessage = "์๋ฒ ์๋ต ์๊ฐ์ด ์ด๊ณผ๋์์ต๋๋ค.";
} else if (error instanceof SSLException) {
userMessage = "๋ณด์ ์ฐ๊ฒฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.";
// ๋ณด์ ์๋ฌ๋ ํน๋ณํ ์ฒ๋ฆฌ
handleSecurityError(error);
} else {
userMessage = "๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.";
}
showUserMessage(userMessage);
logError(error);
}
// ์ธ์ฆ ์๋ฌ ์ฒ๋ฆฌ
public void handleAuthError(int errorCode) {
switch (errorCode) {
case 401: // Unauthorized
// ํ ํฐ ๋ง๋ฃ - ์๋ ๊ฐฑ์ ์๋
refreshTokenAndRetry();
break;
case 403: // Forbidden
// ๊ถํ ์์ - ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก
showUserMessage("์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค.");
redirectToLogin();
break;
case 429: // Too Many Requests
// ์์ฒญ ์ ํ - ์ฌ์๋ ์ง์ฐ
showUserMessage("์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.");
break;
default:
showUserMessage("์ธ์ฆ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
}
}
private void handleSecurityError(Throwable error) {
// ๋ณด์ ์๋ฌ ๋ฐ์ ์ ํน๋ณํ ์ฒ๋ฆฌ
// 1. ์ฌ์ฉ์ ์ธ์
์ข
๋ฃ
clearUserSession();
// 2. ๋ณด์ ํ์ ์๋ฆผ
notifySecurityTeam(error);
// 3. ์ฑ ์ฌ์์ ๋๋ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก
redirectToLogin();
}
private void sendErrorToServer(Exception e) {
// ์๋ฒ๋ก ์๋ฌ ๋ก๊ทธ ์ ์ก
// (Crashlytics, Sentry ๋ฑ ์ฌ์ฉ)
}
}
๋ณด์์ ํ ๋ฒ ํ๊ณ ๋๋๋ ๊ฒ ์๋์ผ. ์๋ก์ด ์ทจ์ฝ์ ์ด ๊ณ์ ๋ฐ๊ฒฌ๋๊ณ , ๊ณต๊ฒฉ ๊ธฐ๋ฒ๋ ์งํํ๋๊น ์ ๊ธฐ์ ์ผ๋ก ์ ๋ฐ์ดํธํด์ผ ํด.
๐ ๋ณด์ ์ ์ง๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
๋งค์ฃผ:
โข ์์กด์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ทจ์ฝ์ ํ์ธ
โข ๋ณด์ ๊ด๋ จ ์ด์ ํธ๋์ปค ํ์ธ
๋งค์:
โข ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฒ์ ์
๋ฐ์ดํธ
โข ๋ณด์ ํจ์น ์ ์ฉ
โข ๋ก๊ทธ ๋ถ์ ๋ฐ ์ด์ ์งํ ํ์ธ
๋ถ๊ธฐ๋ณ:
โข ์ ์ฒด ๋ณด์ ๊ฐ์ฌ
โข ์นจํฌ ํ
์คํธ ์ํ
โข ๋ณด์ ์ ์ฑ
๊ฒํ ๋ฐ ์
๋ฐ์ดํธ
์ฐ๊ฐ:
โข ์ธ๋ถ ๋ณด์ ์ ๋ฌธ๊ฐ ๊ฐ์ฌ
โข ๋ณด์ ๊ต์ก ์ค์
โข ์ฌํด ๋ณต๊ตฌ ๊ณํ ํ
์คํธ
ํ ์ ์ฒด๊ฐ ๋ณด์์ ์ค์์ฑ์ ์ธ์ํ๊ณ ์ค์ฒํด์ผ ํด. ํผ์์๋ ์๋ฒฝํ ๋ณด์์ ๋ง๋ค ์ ์์ด.
โจ ๋ณด์ ๋ฌธํ ์กฐ์ฑ ํ
1. ์ฝ๋ ๋ฆฌ๋ทฐ์ ๋ณด์ ์ฒดํฌ ํฌํจ: ๋ชจ๋ PR์์ ๋ณด์ ๊ด๋ จ ํญ๋ชฉ์ ํ์ธํด.
2. ๋ณด์ ์ฑํผ์ธ ์ง์ : ํ์์ ๋ณด์ ์ ๋ฌธ๊ฐ๋ฅผ ์ง์ ํ๊ณ , ๋ค๋ฅธ ํ์๋ค์ ๊ต์กํด.
3. ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ต์ก: ์ต์ ๋ณด์ ํธ๋ ๋์ ๊ณต๊ฒฉ ๊ธฐ๋ฒ์ ๊ณต์ ํด.
4. ๋ณด์ ์ด์ ๋ณด์ ์ ๋: ๋ณด์ ์ทจ์ฝ์ ์ ๋ฐ๊ฒฌํ๊ณ ์์ ํ ํ์์๊ฒ ์ธ์ผํฐ๋ธ๋ฅผ ์ค.
5. ์คํจ ์ฌ๋ก ๊ณต์ : ๋ค๋ฅธ ํ์ฌ์ ๋ณด์ ์ฌ๊ณ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ๊ตํ์ ์ป์ด.
๐ ๋ง์น๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์๋๋ก์ด๋ ์ฑ ๋ณด์ ์ฝ๋ฉ์ ๋ํด ๊ฝค ๋ง์ ๊ฑธ ๋ฐฐ์ ์ ๊ฑฐ์ผ. ํ์ง๋ง ๊ธฐ์ตํด, ์ด๊ฑด ์์์ผ ๋ฟ์ด์ผ. ๋ณด์์ ๋์๋ ์ฌ์ ์ด๊ฑฐ๋ .
์ฒ์์๋ ๋ณต์กํ๊ณ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ด. "์ด๋ ๊ฒ๊น์ง ํด์ผ ํ๋?" ์ถ์ ๋๋ ์์ ๊ฑฐ์ผ. ํ์ง๋ง ์ฌ์ฉ์๋ค์ ๊ฐ์ธ์ ๋ณด์ ์์ฐ์ ๋ณดํธํ๋ ๊ฑด ๊ฐ๋ฐ์์ ์ค์ํ ์ฑ
์์ด์ผ. ๐ช
์ค๋ ๋ฐฐ์ด ๋ด์ฉ๋ค์ ํ๋์ฉ ์ค์ฒํด๋ณด์. ํ ๋ฒ์ ๋ค ์ ์ฉํ๋ ค๊ณ ํ์ง ๋ง๊ณ , ๊ฐ์ฅ ์ค์ํ ๊ฒ๋ถํฐ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ฐ์ ํด๋๊ฐ๋ฉด ๋ผ. ์๋ฅผ ๋ค์ด:
1์ฃผ์ฐจ: ๋ฐ์ดํฐ ์ํธํ ์ ์ฉ (EncryptedSharedPreferences)
2์ฃผ์ฐจ: ๋คํธ์ํฌ ๋ณด์ ๊ฐํ (HTTPS, Certificate Pinning)
3์ฃผ์ฐจ: ์์ฒด ์ธ์ฆ ๊ตฌํ
4์ฃผ์ฐจ: ์ฝ๋ ๋๋
ํ ๋ฐ ๋ฃจํ
ํ์ง
5์ฃผ์ฐจ: ์ปดํฌ๋ํธ ๋ณด์ ์ ๊ฒ
6์ฃผ์ฐจ: ๋ณด์ ํ
์คํธ ๋ฐ ์ทจ์ฝ์ ์์
๊ทธ๋ฆฌ๊ณ ํผ์ ๊ณ ๋ฏผํ์ง ๋ง! ๋ณด์์ ํผ์์ ์๋ฒฝํ๊ฒ ํ ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ณด์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์ ์๋ ์๊ณ , ์ปค๋ฎค๋ํฐ์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ง์์ ๊ณต์ ํ ์๋ ์์ด. ์ค์ ๋ก ์ฌ๋ฅ๋ท์๋ ๋ชจ๋ฐ์ผ ๋ณด์ ์ ๋ฌธ๊ฐ๋ค์ด ๋ง์ด ํ๋ํ๊ณ ์์ผ๋, ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ๋์์ ์์ฒญํด๋ด! ๐ค
๋ง์ง๋ง์ผ๋ก, ๋ณด์์ ๋จ์ํ ๊ธฐ์ ์ ์ธ ๋ฌธ์ ๊ฐ ์๋๋ผ ์ฌ์ฉ์์ ๋ํ ์กด์ค์ด๋ผ๋ ๊ฑธ ๊ธฐ์ตํด. ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์ฑ์ ๋ฏฟ๊ณ ์ฌ์ฉํ๋ ์ฌ์ฉ์๋ค์ ์ํด, ์ต์ ์ ๋คํด ์์ ํ ์ฑ์ ๋ง๋ค์! ๐ก๏ธ
์์ผ๋ก๋ ๊ณ์ ๊ณต๋ถํ๊ณ , ์ค์ฒํ๊ณ , ๊ฐ์ ํด๋๊ฐ์. ๋๋ผ๋ฉด ์ถฉ๋ถํ ํ ์ ์์ด! ํ์ดํ
! ๐ชโจ
๐ ์ถ๊ฐ ํ์ต ์๋ฃ
๊ณต์ ๋ฌธ์:
โข Android Security Best Practices (developer.android.com)
โข OWASP Mobile Security Testing Guide
โข Google Play Security Guidelines
์จ๋ผ์ธ ๊ฐ์:
โข Udemy: Android Security Masterclass
โข Coursera: Mobile Application Security
โข Pluralsight: Securing Android Applications
์ปค๋ฎค๋ํฐ:
โข Stack Overflow (android-security ํ๊ทธ)
โข Reddit r/androiddev
โข XDA Developers Forum
๋๊ตฌ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ:
โข Android Jetpack Security
โข SQLCipher for Android
โข Google Play Integrity API
โข Firebase App Check
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.