클라이언트 앱 보안 가이드
작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Browser Extension, Tauri Desktop App, Flutter Mobile App
1. 코드 서명
플랫폼별 코드 서명 요구사항
| 플랫폼 | 서명 필수 여부 | 인증서 유형 | 비용 |
|---|---|---|---|
| Chrome Web Store | O (자동) | Google 서명 | 무료 (등록비 $5) |
| Firefox Add-ons | O (자동) | Mozilla 서명 | 무료 |
| macOS (Tauri) | O (필수) | Apple Developer ID | $99/년 |
| Windows (Tauri) | 권 장 (SmartScreen) | EV Code Signing | $200-400/년 |
| Linux (Tauri) | 선택적 | GPG 서명 | 무료 |
| iOS (Flutter) | O (필수) | Apple Distribution | $99/년 |
| Android (Flutter) | O (필수) | Keystore (자체 서명) | 무료 (Play 등록비 $25) |
macOS 코드 서명 + Notarization
#!/bin/bash
# scripts/sign-macos.sh
# 1. 코드 서명
codesign --deep --force --verify --verbose \
--sign "Developer ID Application: Company Name (TEAM_ID)" \
--options runtime \
--entitlements entitlements.plist \
target/release/bundle/macos/MyApp.app
# 2. 공증(Notarization)
xcrun notarytool submit target/release/bundle/macos/MyApp.dmg \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" \
--wait
# 3. Staple (오프라인 검증용)
xcrun stapler staple target/release/bundle/macos/MyApp.dmg
Windows 코드 서명
#!/bin/bash
# scripts/sign-windows.sh
# EV 인증서 (USB 토큰) 또는 Azure Key Vault 사용
signtool sign /tr http://timestamp.digicert.com /td sha256 \
/fd sha256 /a \
target/release/bundle/nsis/MyApp_x64-setup.exe
# Azure Trusted Signing (CI/CD 환경 권장)
# azure-cli + Trusted Signing 서비스 사용
Android 앱 서명
# Android App Bundle 서명
# Play App Signing 사용 시 (권장): Google이 배포용 서명 관리
# 업로드 키만 로 컬에서 관리
# key.properties (Git 제외!)
storePassword=***
keyPassword=***
keyAlias=upload
storeFile=../keys/upload-keystore.jks
iOS 앱 서명
# Xcode Managed Signing (권장) 또는 Manual
# CI/CD: fastlane match 사용
# Fastlane Matchfile
git_url("https://github.com/company/certificates")
storage_mode("git")
type("appstore")
app_identifier("com.example.app")
2. CSP (Content Security Policy)
Browser Extension CSP
// manifest.json (Manifest V3)
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'",
"sandbox": "sandbox allow-scripts; script-src 'self'"
}
}
Manifest V3 제한사항
| 제한 | 설명 |
|---|---|
eval() 금지 | 동적 코드 실행 불가 |
new Function() 금지 | 문자열에서 함수 생성 불가 |
| 원격 스크립트 금지 | <script src="https://..."> 사용 불가 |
| 인라인 스크립트 금지 | <script>...</script> 사용 불가 |
'unsafe-eval' 금지 | CSP에서 허용 불가 |
Tauri CSP
// src-tauri/tauri.conf.json
{
"app": {
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://api.example.com; connect-src 'self' https://api.example.com wss://ws.example.com; font-src 'self'"
}
}
}
Tauri v2 Capabilities (권한 시스템)
// src-tauri/capabilities/default.json
{
"identifier": "default",
"description": "기본 권한",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"dialog:allow-open",
"notification:default",
"clipboard-manager:allow-write",
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.example.com/**" }
]
},
{
"identifier": "fs:allow-read",
"allow": [
{ "path": "$APPDATA/**" }
]
}
]
}
CSP 설정 체크리스트
| 디렉티브 | 권장 값 | 설명 |
|---|---|---|
default-src | 'self' | 기본 제한 |
script-src | 'self' | 스크립트 소스 |
style-src | 'self' 'unsafe-inline' | 스타일 (프레임워크 요구 시) |
img-src | 'self' https: data: | 이미지 소스 |
connect-src | 'self' https://api.example.com | API 엔드포인트 |
font-src | 'self' | 폰트 소스 |
object-src | 'none' | 플러그인 (Flash 등) 차단 |
frame-src | 'none' | iframe 차단 |
base-uri | 'self' | base 태그 제한 |
3. 난독화 / 소스 보호
플랫폼별 난독화 전략
| 플랫폼 | 도구 | 수준 | 비고 |
|---|---|---|---|
| Browser Ext (JS) | Terser (기본 minify) | 낮음 | CWS 정책상 과도한 난독화 금지 |
| Browser Ext (WASM) | Rust -> WASM | 높음 | 핵심 로직 WASM으로 이동 |
| Tauri (Rust) | 네이티브 컴파일 | 높음 | 바이너리로 배포, 리버스 엔지니어링 난이도 높음 |
| Tauri (JS) | Terser + 소스맵 제거 | 중간 | 프론트엔드 코드 |
| Flutter (iOS) | AOT 컴파일 | 높음 | 네이티브 바이너리 |
| Flutter (Android) | R8/ProGuard + 난독화 | 높음 | --obfuscate --split-debug-info |
Chrome Web Store 난독화 정책
| 허용 | 금지 |
|---|---|
| Minification (공백/주석 제거) | 변수명 의미 없는 치환 (과도) |
| Tree-shaking | 코드 흐름 변형 |
| 번들링 | 문자열 암호화 |
| 타입 제거 (TypeScript) | 안티 디버깅 코드 |
Chrome Web Store는 "코드를 읽을 수 있어야 한다"는 정책. 소스맵을 함께 제출하거나, 원본 코드 제출 요구 시 제공 가능해야 함.
Flutter 난독화 빌드
# Android: 난독화 + 디버그 심볼 분리
flutter build appbundle \
--obfuscate \
--split-debug-info=build/debug-info/
# iOS: AOT 컴파일 (자동 난독화 수준 높음)
flutter build ipa \
--obfuscate \
--split-debug-info=build/debug-info/
# 디버그 심볼은 크래시 리포팅 서비스에 업로드
# Firebase Crashlytics, Sentry 등
4. API 키 보호
핵심 원칙: 클라이언트에 시크릿 키를 절대 포함하지 않는다
| 키 유형 | 클라이언트 포함 가능 | 보호 방법 |
|---|---|---|
| OAuth Client ID | O | 공개 식별자 (시크릿 아님) |
| OAuth Client Secret | X | 서버 사이드만 |
| API Secret Key | X | 프록시 서버 경유 |
| Firebase Config | O (제한적) | 보안 규칙으로 접근 제어 |
| 서드파티 API 키 | X | 백엔드 프록시 |
| 암호화 키 | X | 서버 사이드 또는 KMS |
안전한 API 키 사용 패턴
클라이언트 앱 백엔드 서버 외부 API
| | |
| --- 요청 (Bearer 토큰) --> | |
| | --- 요청 (API Key) --> |
| | <-- 응답 ------------ |
| <-- 응답 (가공된 데이터) -- | |
환경 변수 관리
// 빌드 타임에 주입 (번들에 포함되므로 공개 가능한 값만)
// vite.config.ts
export default defineConfig({
define: {
__API_BASE_URL__: JSON.stringify(process.env.VITE_API_BASE_URL),
__OAUTH_CLIENT_ID__: JSON.stringify(process.env.VITE_OAUTH_CLIENT_ID),
// 절대 시크릿 키를 여기에 포함하지 않음
},
});
// Flutter: --dart-define 또는 .env
// 빌드 시 주입 (공개 가능한 값만)
flutter build apk \
--dart-define=API_BASE_URL=https://api.example.com \
--dart-define=OAUTH_CLIENT_ID=public_client_id
5. 안전한 통신
TLS/HTTPS 요구사항
| 요구사항 | 설명 |
|---|---|
| HTTPS 필수 | 모든 API 통신은 HTTPS만 허용 |
| TLS 1.2+ | TLS 1.0/1.1 비활성화 |
| 인증서 검증 | 시스템 인증서 저장소 사용 |
| 평문 HTTP 금지 | 개발 환경도 HTTPS 권장 |
Certificate Pinning
| 플랫폼 | 구현 방법 | 권장도 |
|---|---|---|
| Browser Ext | 불가 (브라우저 관리) | - |
| Tauri | Rust reqwest + 커스텀 인증서 검증 | 선택 |
| Flutter (Android) | network_security_config.xml | 권장 |
| Flutter (iOS) | TrustKit 또는 ssl_pinning_plugin | 권장 |
Android Network Security Config
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
<!-- 프로덕션: 핀 고정 -->
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
<!-- 개발: 평문 허용 (로컬만) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">10.0.2.2</domain>
</domain-config>
</network-security-config>
CORS 설정 (Laravel 백엔드)
// config/cors.php
return [
'paths' => ['api/*', 'oauth/*'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
'allowed_origins' => [
'chrome-extension://<extension-id>',
'tauri://localhost', // Tauri v1
'https://tauri.localhost', // Tauri v2
'http://localhost:*', // 개발용
],
'allowed_headers' => ['*'],
'exposed_headers' => ['X-Request-Id'],
'max_age' => 86400,
'supports_credentials' => true,
];
6. 데이터 암호화 (로컬 저장소)
저장소별 암호화 전략
| 저장소 | Browser Ext | Tauri | Flutter |
|---|---|---|---|
| 인증 토큰 | chrome.storage.session | OS Keychain | flutter_secure_storage |
| 사용자 설정 | chrome.storage.local | tauri-plugin-store (암호화 옵션) | shared_preferences (비민감) |
| 캐시 데이터 | IndexedDB | SQLite (sqlcipher) | sqflite + 암호화 |
| 파일 | - | 파일 시스템 암호화 | 파일 시스템 암호화 |
암호화 대상 분류
| 민감도 | 데이터 예시 | 저장소 | 암호화 |
|---|---|---|---|
| 높음 | 토큰, 비밀번호, 개인정보 | 보안 저장소 (OS) | 필수 |
| 중간 | 사용자 설정, 검색 기록 | 암호화 DB | 권장 |
| 낮음 | UI 상태, 테마 설정 | 일반 저장소 | 불필요 |
| 캐시 | API 응답 캐시 | 임시 저장소 | 선택 |
SQLCipher (Tauri/Flutter)
// Tauri - better-sqlite3 + sqlcipher
import Database from 'better-sqlite3';
const db = new Database('app.db');
db.pragma(`key='${encryptionKey}'`); // 암호화 키 설정
db.pragma('cipher_compatibility = 4');
// Flutter - sqflite_sqlcipher
import 'package:sqflite_sqlcipher/sqflite_sqlcipher.dart';
final db = await openDatabase(
'app.db',
password: encryptionKey, // 암호화 키
version: 1,
onCreate: (db, version) async {
// 테이블 생성
},
);
7. 권한 최소화 원칙
Browser Extension 권한
// manifest.json - 최소 권한
{
"permissions": [
"storage", // 데이터 저장 (거의 필수)
"identity" // OAuth 인증 (필요 시)
],
"optional_permissions": [
"tabs", // 탭 정보 (사용자 동의 후)
"notifications", // 알림 (사용자 동의 후)
"clipboardWrite" // 클립보드 (사용자 동의 후)
],
"host_permissions": [
"https://api.example.com/*" // 특정 도메인만
],
"optional_host_permissions": [
"https://*/*" // 추가 사이트 (사용자 동의 후)
]
}
Browser Extension 권한 심사 영향
| 권한 | 심사 영향 | 대안 |
|---|---|---|
<all_urls> | 매우 높음 (심사 지연) | 특정 도메인만 지정 |
tabs | 높음 | activeTab으로 대체 |
webRequest | 높음 | declarativeNetRequest으로 대체 |
cookies | 높음 | 세션 스토리지 활용 |
storage | 낮음 | - |
notifications | 낮음 | - |
activeTab | 낮음 | 권장 |
Flutter 권한 (iOS/Android)
<!-- Android: AndroidManifest.xml - 필요한 것만 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 아래는 필요할 때만 추가 -->
<!-- <uses-permission android:name="android.permission.CAMERA" /> -->
<!-- <uses-permission android:name="android.permission.READ_CONTACTS" /> -->
<!-- iOS: Info.plist - 사용 사유 필수 기재 -->
<key>NSCameraUsageDescription</key>
<string>프로필 사진 촬영을 위해 카메라 접근이 필요합니다</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>프로필 사진 선택을 위해 사진 라이브러리 접근이 필 요합니다</string>
Tauri v2 권한 (Capabilities)
// 최소 권한 원칙 적용
{
"identifier": "main-window",
"description": "메인 윈도우 권한",
"windows": ["main"],
"permissions": [
"core:default",
// 네트워크: 특정 도메인만
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.example.com/**" }
],
"deny": [
{ "url": "http://**" }
]
},
// 파일시스템: 앱 데이터 폴더만
{
"identifier": "fs:allow-read",
"allow": [{ "path": "$APPDATA/**" }]
},
// 쉘: 특정 명령만
{
"identifier": "shell:allow-open",
"allow": [
{ "cmd": "open", "args": ["https://*"] }
]
}
]
}