timsquad 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +288 -0
- package/README.md +158 -151
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +170 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +94 -5
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/skills.d.ts +12 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +231 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/upgrade.js +5 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/daemon/entry.js +3 -3
- package/dist/daemon/entry.js.map +1 -1
- package/dist/daemon/index.d.ts +3 -2
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +137 -45
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/meta-cache.d.ts +1 -0
- package/dist/daemon/meta-cache.d.ts.map +1 -1
- package/dist/daemon/meta-cache.js +9 -0
- package/dist/daemon/meta-cache.js.map +1 -1
- package/dist/daemon/session-state.d.ts +19 -0
- package/dist/daemon/session-state.d.ts.map +1 -0
- package/dist/daemon/session-state.js +132 -0
- package/dist/daemon/session-state.js.map +1 -0
- package/dist/daemon/shutdown.d.ts.map +1 -1
- package/dist/daemon/shutdown.js +7 -1
- package/dist/daemon/shutdown.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compile-rules.d.ts +66 -0
- package/dist/lib/compile-rules.d.ts.map +1 -0
- package/dist/lib/compile-rules.js +114 -0
- package/dist/lib/compile-rules.js.map +1 -0
- package/dist/lib/compiler.d.ts +105 -0
- package/dist/lib/compiler.d.ts.map +1 -0
- package/dist/lib/compiler.js +368 -0
- package/dist/lib/compiler.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/template.d.ts.map +1 -1
- package/dist/lib/template.js +6 -0
- package/dist/lib/template.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +12 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +2 -0
- package/dist/types/project.js.map +1 -1
- package/package.json +1 -1
- package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
- package/templates/base/skills/controller/SKILL.md +111 -0
- package/templates/base/skills/controller/references/README.md +35 -0
- package/templates/base/skills/controller/rules/README.md +18 -0
- package/templates/base/skills/mobile/dart/SKILL.md +69 -0
- package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
- package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
- package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
- package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
- package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
- package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
- package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
- package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
- package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
- package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
- package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
- package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
- package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
- package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
- package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
- package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
- package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
- package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
- package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
- package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
- package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
- package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
- package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
- package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
- package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
- package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
- package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
- package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
- package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
- package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
- package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
- package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
- package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
- package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
- package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
- package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
- package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
- package/templates/platforms/claude-code/scripts/completion-guard.sh +57 -0
- package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
- package/templates/platforms/claude-code/settings.json +75 -3
- package/templates/project-types/mobile-app/config.yaml +123 -0
- package/templates/project-types/mobile-app/process/workflow.xml +191 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Data Protection
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: "미적용 → 로컬 DB 평문 노출, 스크린샷 유출, 클립보드 탈취"
|
|
5
|
+
tags: encryption, sqlcipher, screenshot, clipboard, flag-secure, screen-blur
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Data Protection
|
|
9
|
+
|
|
10
|
+
**Impact: MEDIUM (미적용 → 로컬 DB 평문 노출, 스크린샷 유출, 클립보드 탈취)**
|
|
11
|
+
|
|
12
|
+
로컬 데이터 암호화, 클립보드 보호, 스크린샷 방지, 백그라운드 스크린 블러.
|
|
13
|
+
저장 데이터와 화면 노출 보호.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
encrypt: ^5.0.3 # AES/RSA 암호화
|
|
21
|
+
sqflite_sqlcipher: ^3.1.0 # SQLCipher (암호화된 SQLite)
|
|
22
|
+
# 또는 sqflite + 수동 암호화
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 로컬 DB 암호화 (SQLCipher)
|
|
26
|
+
|
|
27
|
+
**Incorrect (평문 SQLite):**
|
|
28
|
+
```dart
|
|
29
|
+
// 기본 sqflite → 평문 저장
|
|
30
|
+
// 루팅 기기에서 DB 파일 복사 → SQLite 브라우저로 열람
|
|
31
|
+
final db = await openDatabase('app.db');
|
|
32
|
+
await db.insert('users', {'name': '홍길동', 'phone': '010-1234-5678'});
|
|
33
|
+
// → /data/data/com.app/databases/app.db 평문
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Correct (SQLCipher 암호화):**
|
|
37
|
+
```dart
|
|
38
|
+
import 'package:sqflite_sqlcipher/sqflite.dart';
|
|
39
|
+
|
|
40
|
+
class EncryptedDatabase {
|
|
41
|
+
static Database? _db;
|
|
42
|
+
|
|
43
|
+
/// 암호화된 DB 열기
|
|
44
|
+
static Future<Database> getInstance() async {
|
|
45
|
+
if (_db != null) return _db!;
|
|
46
|
+
|
|
47
|
+
// DB 암호화 키 — flutter_secure_storage에서 가져오기
|
|
48
|
+
final storage = const FlutterSecureStorage();
|
|
49
|
+
var dbKey = await storage.read(key: 'db_encryption_key');
|
|
50
|
+
|
|
51
|
+
if (dbKey == null) {
|
|
52
|
+
// 최초 실행: 랜덤 키 생성 후 안전 저장
|
|
53
|
+
dbKey = _generateRandomKey();
|
|
54
|
+
await storage.write(key: 'db_encryption_key', value: dbKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_db = await openDatabase(
|
|
58
|
+
'app_encrypted.db',
|
|
59
|
+
password: dbKey, // SQLCipher 암호화 키
|
|
60
|
+
version: 1,
|
|
61
|
+
onCreate: (db, version) async {
|
|
62
|
+
await db.execute('''
|
|
63
|
+
CREATE TABLE users (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
name TEXT NOT NULL,
|
|
66
|
+
phone TEXT,
|
|
67
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
68
|
+
)
|
|
69
|
+
''');
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return _db!;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static String _generateRandomKey() {
|
|
77
|
+
final random = Random.secure();
|
|
78
|
+
final bytes = List<int>.generate(32, (_) => random.nextInt(256));
|
|
79
|
+
return base64Encode(bytes);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 필드 레벨 암호화 (encrypt 패키지)
|
|
85
|
+
|
|
86
|
+
```dart
|
|
87
|
+
import 'package:encrypt/encrypt.dart' as encrypt;
|
|
88
|
+
|
|
89
|
+
class FieldEncryption {
|
|
90
|
+
late final encrypt.Encrypter _encrypter;
|
|
91
|
+
late final encrypt.IV _iv;
|
|
92
|
+
|
|
93
|
+
FieldEncryption({required String key}) {
|
|
94
|
+
// AES-256 키 (32바이트)
|
|
95
|
+
final aesKey = encrypt.Key.fromUtf8(key.padRight(32).substring(0, 32));
|
|
96
|
+
_iv = encrypt.IV.fromLength(16);
|
|
97
|
+
_encrypter = encrypt.Encrypter(encrypt.AES(aesKey));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// 민감 필드 암호화
|
|
101
|
+
String encryptField(String plaintext) {
|
|
102
|
+
return _encrypter.encrypt(plaintext, iv: _iv).base64;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// 복호화
|
|
106
|
+
String decryptField(String encrypted) {
|
|
107
|
+
return _encrypter.decrypt64(encrypted, iv: _iv);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 사용 예시
|
|
112
|
+
final encryption = FieldEncryption(key: await getEncryptionKey());
|
|
113
|
+
final encryptedPhone = encryption.encryptField('010-1234-5678');
|
|
114
|
+
// DB에 암호화된 값 저장
|
|
115
|
+
await db.insert('users', {'phone': encryptedPhone});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 스크린샷 방지
|
|
119
|
+
|
|
120
|
+
```dart
|
|
121
|
+
/// Android: FLAG_SECURE 설정
|
|
122
|
+
/// iOS: 화면 캡처 감지 + 오버레이
|
|
123
|
+
class ScreenshotProtection {
|
|
124
|
+
|
|
125
|
+
/// Android: FLAG_SECURE (스크린샷 + 화면 녹화 차단)
|
|
126
|
+
static Future<void> enableAndroid() async {
|
|
127
|
+
if (Platform.isAndroid) {
|
|
128
|
+
// MethodChannel로 네이티브 호출
|
|
129
|
+
const channel = MethodChannel('com.app/security');
|
|
130
|
+
await channel.invokeMethod('enableSecureFlag');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static Future<void> disableAndroid() async {
|
|
135
|
+
if (Platform.isAndroid) {
|
|
136
|
+
const channel = MethodChannel('com.app/security');
|
|
137
|
+
await channel.invokeMethod('disableSecureFlag');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Android 네이티브 (MainActivity.kt)
|
|
143
|
+
// class MainActivity: FlutterActivity() {
|
|
144
|
+
// override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
|
145
|
+
// super.configureFlutterEngine(flutterEngine)
|
|
146
|
+
// MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.app/security")
|
|
147
|
+
// .setMethodCallHandler { call, result ->
|
|
148
|
+
// when (call.method) {
|
|
149
|
+
// "enableSecureFlag" -> {
|
|
150
|
+
// window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
151
|
+
// result.success(null)
|
|
152
|
+
// }
|
|
153
|
+
// "disableSecureFlag" -> {
|
|
154
|
+
// window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
155
|
+
// result.success(null)
|
|
156
|
+
// }
|
|
157
|
+
// }
|
|
158
|
+
// }
|
|
159
|
+
// }
|
|
160
|
+
// }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 백그라운드 스크린 블러
|
|
164
|
+
|
|
165
|
+
```dart
|
|
166
|
+
/// 앱 백그라운드 진입 시 민감 화면 가리기 (앱 스위처에서 노출 방지)
|
|
167
|
+
class AppLifecycleObserver extends WidgetsBindingObserver {
|
|
168
|
+
final GlobalKey<NavigatorState> navigatorKey;
|
|
169
|
+
OverlayEntry? _overlayEntry;
|
|
170
|
+
|
|
171
|
+
AppLifecycleObserver({required this.navigatorKey});
|
|
172
|
+
|
|
173
|
+
@override
|
|
174
|
+
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
175
|
+
switch (state) {
|
|
176
|
+
case AppLifecycleState.inactive:
|
|
177
|
+
case AppLifecycleState.paused:
|
|
178
|
+
// 앱 스위처에 표시될 때 오버레이
|
|
179
|
+
_showSecurityOverlay();
|
|
180
|
+
case AppLifecycleState.resumed:
|
|
181
|
+
_removeSecurityOverlay();
|
|
182
|
+
default:
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void _showSecurityOverlay() {
|
|
188
|
+
_overlayEntry = OverlayEntry(
|
|
189
|
+
builder: (_) => Container(
|
|
190
|
+
color: Colors.white, // 또는 앱 로고가 있는 스플래시
|
|
191
|
+
child: const Center(child: FlutterLogo(size: 100)),
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
navigatorKey.currentState?.overlay?.insert(_overlayEntry!);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void _removeSecurityOverlay() {
|
|
198
|
+
_overlayEntry?.remove();
|
|
199
|
+
_overlayEntry = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 클립보드 보호
|
|
205
|
+
|
|
206
|
+
```dart
|
|
207
|
+
/// 민감 데이터 복사 후 자동 클리어
|
|
208
|
+
class ClipboardProtection {
|
|
209
|
+
static Timer? _clearTimer;
|
|
210
|
+
|
|
211
|
+
/// 민감 데이터를 클립보드에 복사 + 자동 클리어
|
|
212
|
+
static Future<void> copyWithAutoClean(
|
|
213
|
+
String data, {
|
|
214
|
+
Duration clearAfter = const Duration(seconds: 30),
|
|
215
|
+
}) async {
|
|
216
|
+
await Clipboard.setData(ClipboardData(text: data));
|
|
217
|
+
|
|
218
|
+
_clearTimer?.cancel();
|
|
219
|
+
_clearTimer = Timer(clearAfter, () async {
|
|
220
|
+
// 클립보드 비우기
|
|
221
|
+
await Clipboard.setData(const ClipboardData(text: ''));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// 즉시 클립보드 클리어
|
|
226
|
+
static Future<void> clearNow() async {
|
|
227
|
+
_clearTimer?.cancel();
|
|
228
|
+
await Clipboard.setData(const ClipboardData(text: ''));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 안전한 텍스트 입력
|
|
234
|
+
|
|
235
|
+
```dart
|
|
236
|
+
/// 민감 입력 필드 (비밀번호, PIN)
|
|
237
|
+
class SecureTextField extends StatelessWidget {
|
|
238
|
+
final TextEditingController controller;
|
|
239
|
+
final String label;
|
|
240
|
+
|
|
241
|
+
const SecureTextField({
|
|
242
|
+
super.key,
|
|
243
|
+
required this.controller,
|
|
244
|
+
required this.label,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
@override
|
|
248
|
+
Widget build(BuildContext context) {
|
|
249
|
+
return TextField(
|
|
250
|
+
controller: controller,
|
|
251
|
+
obscureText: true,
|
|
252
|
+
enableSuggestions: false, // 자동 완성 비활성화
|
|
253
|
+
autocorrect: false, // 자동 수정 비활성화
|
|
254
|
+
enableIMEPersonalizedLearning: false, // 키보드 학습 비활성화
|
|
255
|
+
decoration: InputDecoration(labelText: label),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 규칙
|
|
262
|
+
|
|
263
|
+
- 로컬 DB 민감 데이터 → SQLCipher 또는 필드 레벨 암호화
|
|
264
|
+
- DB 암호화 키 → `flutter_secure_storage`에 저장 (코드 하드코딩 금지)
|
|
265
|
+
- 최초 실행 → 랜덤 키 생성 후 안전 저장소에 보관
|
|
266
|
+
- 스크린샷 방지 → Android `FLAG_SECURE`, iOS 오버레이 방식
|
|
267
|
+
- 민감 화면만 선별 적용 (전체 앱 적용 시 UX 저하)
|
|
268
|
+
- 앱 백그라운드 → 스크린 블러/오버레이 (앱 스위처 노출 방지)
|
|
269
|
+
- 클립보드 → 민감 데이터 복사 후 30초 내 자동 클리어
|
|
270
|
+
- 비밀번호/PIN 입력 → `enableSuggestions: false` + `autocorrect: false`
|
|
271
|
+
- `enableIMEPersonalizedLearning: false` → 키보드 학습에 민감 입력 미포함
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Code Obfuscation
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: "미적용 → 앱 디컴파일 시 비즈니스 로직/API 엔드포인트 노출"
|
|
5
|
+
tags: obfuscation, proguard, r8, dsym, split-debug-info, crashlytics
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Code Obfuscation
|
|
9
|
+
|
|
10
|
+
**Impact: HIGH (미적용 → 앱 디컴파일 시 비즈니스 로직/API 엔드포인트 노출)**
|
|
11
|
+
|
|
12
|
+
Dart 난독화, Android ProGuard/R8 shrinking, iOS dSYM 관리,
|
|
13
|
+
Crashlytics 디버그 심볼 업로드. 릴리즈 빌드 보호.
|
|
14
|
+
|
|
15
|
+
### Dart 난독화
|
|
16
|
+
|
|
17
|
+
**Incorrect (난독화 없이 릴리즈 빌드):**
|
|
18
|
+
```bash
|
|
19
|
+
flutter build apk --release
|
|
20
|
+
# → Dart 코드 디컴파일 시 함수명/클래스명 그대로 노출
|
|
21
|
+
# → 비즈니스 로직, API 엔드포인트, 검증 로직 분석 가능
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct (난독화 + 디버그 심볼 분리):**
|
|
25
|
+
```bash
|
|
26
|
+
# APK (Android)
|
|
27
|
+
flutter build apk --release \
|
|
28
|
+
--obfuscate \
|
|
29
|
+
--split-debug-info=build/debug-info/android
|
|
30
|
+
|
|
31
|
+
# App Bundle (Android — Play Store 권장)
|
|
32
|
+
flutter build appbundle --release \
|
|
33
|
+
--obfuscate \
|
|
34
|
+
--split-debug-info=build/debug-info/android
|
|
35
|
+
|
|
36
|
+
# iOS
|
|
37
|
+
flutter build ipa --release \
|
|
38
|
+
--obfuscate \
|
|
39
|
+
--split-debug-info=build/debug-info/ios
|
|
40
|
+
|
|
41
|
+
# --obfuscate: Dart 심볼 이름 난독화 (클래스, 함수, 변수명)
|
|
42
|
+
# --split-debug-info: 디버그 심볼을 별도 디렉토리에 분리 (크래시 분석용)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 디버그 심볼 보관
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# 디버그 심볼 구조
|
|
49
|
+
build/debug-info/
|
|
50
|
+
├── android/
|
|
51
|
+
│ ├── app.android-arm.symbols # ARM32
|
|
52
|
+
│ ├── app.android-arm64.symbols # ARM64
|
|
53
|
+
│ └── app.android-x64.symbols # x86_64
|
|
54
|
+
└── ios/
|
|
55
|
+
└── app.ios-arm64.symbols # iOS ARM64
|
|
56
|
+
|
|
57
|
+
# 버전별 보관 (크래시 리포트 해석에 필수)
|
|
58
|
+
# CI/CD에서 자동 아카이브 권장
|
|
59
|
+
mkdir -p symbols/v1.2.3
|
|
60
|
+
cp -r build/debug-info/* symbols/v1.2.3/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Android ProGuard/R8
|
|
64
|
+
|
|
65
|
+
```groovy
|
|
66
|
+
// android/app/build.gradle
|
|
67
|
+
android {
|
|
68
|
+
buildTypes {
|
|
69
|
+
release {
|
|
70
|
+
// R8 (ProGuard 후속) — 코드 축소 + 난독화
|
|
71
|
+
minifyEnabled true
|
|
72
|
+
shrinkResources true
|
|
73
|
+
|
|
74
|
+
proguardFiles(
|
|
75
|
+
getDefaultProguardFile('proguard-android-optimize.txt'),
|
|
76
|
+
'proguard-rules.pro'
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
# android/app/proguard-rules.pro
|
|
85
|
+
|
|
86
|
+
# Flutter 관련 유지
|
|
87
|
+
-keep class io.flutter.** { *; }
|
|
88
|
+
-keep class io.flutter.plugins.** { *; }
|
|
89
|
+
|
|
90
|
+
# firebase_messaging 관련
|
|
91
|
+
-keep class com.google.firebase.messaging.** { *; }
|
|
92
|
+
|
|
93
|
+
# flutter_secure_storage 관련
|
|
94
|
+
-keep class com.it_nomads.fluttersecurestorage.** { *; }
|
|
95
|
+
|
|
96
|
+
# Gson / JSON 직렬화 (사용 시)
|
|
97
|
+
-keep class * implements com.google.gson.TypeAdapterFactory
|
|
98
|
+
-keep class * implements com.google.gson.JsonSerializer
|
|
99
|
+
-keep class * implements com.google.gson.JsonDeserializer
|
|
100
|
+
|
|
101
|
+
# Crashlytics
|
|
102
|
+
-keepattributes SourceFile,LineNumberTable
|
|
103
|
+
-keep public class * extends java.lang.Exception
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### iOS dSYM
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Xcode → Build Settings
|
|
110
|
+
# DEBUG_INFORMATION_FORMAT = dwarf-with-dsym (Release)
|
|
111
|
+
# STRIP_SWIFT_SYMBOLS = YES
|
|
112
|
+
|
|
113
|
+
# dSYM 위치 (Xcode 빌드 후)
|
|
114
|
+
# ~/Library/Developer/Xcode/DerivedData/Runner-xxx/Build/Products/Release-iphoneos/Runner.app.dSYM
|
|
115
|
+
|
|
116
|
+
# Archive 빌드 시 자동 포함
|
|
117
|
+
# Xcode → Product → Archive → dSYM 포함
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Crashlytics 심볼 업로드
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
# pubspec.yaml
|
|
124
|
+
dependencies:
|
|
125
|
+
firebase_crashlytics: ^4.1.0
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Dart 난독화 심볼 업로드
|
|
130
|
+
firebase crashlytics:symbols:upload \
|
|
131
|
+
--app=1:123456:android:abcdef \
|
|
132
|
+
build/debug-info/android/
|
|
133
|
+
|
|
134
|
+
# iOS dSYM 업로드 (Fastlane 사용 시)
|
|
135
|
+
# fastlane에서 자동 업로드 설정 또는:
|
|
136
|
+
firebase crashlytics:symbols:upload \
|
|
137
|
+
--app=1:123456:ios:abcdef \
|
|
138
|
+
build/debug-info/ios/
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```dart
|
|
142
|
+
// Crashlytics 초기화 (main.dart)
|
|
143
|
+
Future<void> main() async {
|
|
144
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
145
|
+
await Firebase.initializeApp();
|
|
146
|
+
|
|
147
|
+
// Crashlytics 에러 핸들링
|
|
148
|
+
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
|
149
|
+
|
|
150
|
+
// 비동기 에러
|
|
151
|
+
PlatformDispatcher.instance.onError = (error, stack) {
|
|
152
|
+
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
|
153
|
+
return true;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
runApp(const ProviderScope(child: MyApp()));
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### CI/CD 통합
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
# GitHub Actions 예시
|
|
164
|
+
- name: Build Release APK
|
|
165
|
+
run: |
|
|
166
|
+
flutter build apk --release \
|
|
167
|
+
--obfuscate \
|
|
168
|
+
--split-debug-info=build/debug-info/android \
|
|
169
|
+
--dart-define=API_KEY=${{ secrets.API_KEY }}
|
|
170
|
+
|
|
171
|
+
- name: Upload debug symbols to Crashlytics
|
|
172
|
+
run: |
|
|
173
|
+
firebase crashlytics:symbols:upload \
|
|
174
|
+
--app=${{ secrets.FIREBASE_APP_ID_ANDROID }} \
|
|
175
|
+
build/debug-info/android/
|
|
176
|
+
|
|
177
|
+
- name: Archive debug symbols
|
|
178
|
+
uses: actions/upload-artifact@v4
|
|
179
|
+
with:
|
|
180
|
+
name: debug-symbols-${{ github.sha }}
|
|
181
|
+
path: build/debug-info/
|
|
182
|
+
retention-days: 90
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 난독화 효과 확인
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# 난독화 전 (디컴파일 시)
|
|
189
|
+
class UserRepository {
|
|
190
|
+
Future<User> fetchUserProfile(String userId) async { ... }
|
|
191
|
+
Future<void> updatePaymentMethod(PaymentCard card) async { ... }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# 난독화 후 (디컴파일 시)
|
|
195
|
+
class a {
|
|
196
|
+
Future<b> c(String d) async { ... }
|
|
197
|
+
Future<void> e(f g) async { ... }
|
|
198
|
+
}
|
|
199
|
+
# → 함수명/클래스명만 난독화, 문자열 리터럴은 그대로
|
|
200
|
+
# → API 키/URL은 별도 보호 필요 (envied obfuscate 등)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 규칙
|
|
204
|
+
|
|
205
|
+
- 릴리즈 빌드 → `--obfuscate --split-debug-info=<path>` 필수
|
|
206
|
+
- `--split-debug-info` → 버전별 보관 (크래시 리포트 해석에 필수)
|
|
207
|
+
- Android → `minifyEnabled true` + `shrinkResources true` (R8)
|
|
208
|
+
- `proguard-rules.pro` → Flutter/Firebase/보안 라이브러리 keep 규칙
|
|
209
|
+
- iOS → `DEBUG_INFORMATION_FORMAT = dwarf-with-dsym` (Release)
|
|
210
|
+
- Crashlytics → 난독화 심볼 업로드 필수 (미업로드 시 크래시 로그 해석 불가)
|
|
211
|
+
- CI/CD → 심볼 아카이브 (최소 90일 보관)
|
|
212
|
+
- 문자열 리터럴 → 난독화 대상 아님 → API 키는 `envied obfuscate` 별도 처리
|
|
213
|
+
- 디버그 빌드 → 난독화 적용하지 않음 (개발 생산성)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Secure Storage
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: "평문 저장 → 루팅/탈옥 시 토큰/시크릿 탈취, 계정 도용"
|
|
5
|
+
tags: flutter_secure_storage, keychain, encrypted-shared-preferences, token
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Secure Storage
|
|
9
|
+
|
|
10
|
+
**Impact: CRITICAL (평문 저장 → 루팅/탈옥 시 토큰/시크릿 탈취, 계정 도용)**
|
|
11
|
+
|
|
12
|
+
flutter_secure_storage를 사용한 민감 데이터 안전 저장.
|
|
13
|
+
iOS Keychain / Android EncryptedSharedPreferences 기반.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
flutter_secure_storage: ^9.2.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 기본 사용
|
|
24
|
+
|
|
25
|
+
**Incorrect (SharedPreferences에 토큰 저장):**
|
|
26
|
+
```dart
|
|
27
|
+
// SharedPreferences → 평문 XML/plist 파일
|
|
28
|
+
// 루팅된 기기에서 파일 탐색기로 즉시 열람 가능
|
|
29
|
+
final prefs = await SharedPreferences.getInstance();
|
|
30
|
+
await prefs.setString('access_token', token);
|
|
31
|
+
await prefs.setString('refresh_token', refreshToken);
|
|
32
|
+
// → /data/data/com.app/shared_prefs/ 에 평문 저장
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct (flutter_secure_storage 사용):**
|
|
36
|
+
```dart
|
|
37
|
+
class SecureStorageService {
|
|
38
|
+
final FlutterSecureStorage _storage;
|
|
39
|
+
|
|
40
|
+
SecureStorageService()
|
|
41
|
+
: _storage = const FlutterSecureStorage(
|
|
42
|
+
aOptions: AndroidOptions(
|
|
43
|
+
encryptedSharedPreferences: true, // EncryptedSharedPreferences 사용
|
|
44
|
+
),
|
|
45
|
+
iOptions: IOSOptions(
|
|
46
|
+
accessibility: KeychainAccessibility.first_unlock_this_device,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// === 토큰 관리 ===
|
|
51
|
+
|
|
52
|
+
Future<void> saveTokens({
|
|
53
|
+
required String accessToken,
|
|
54
|
+
required String refreshToken,
|
|
55
|
+
}) async {
|
|
56
|
+
await Future.wait([
|
|
57
|
+
_storage.write(key: _Keys.accessToken, value: accessToken),
|
|
58
|
+
_storage.write(key: _Keys.refreshToken, value: refreshToken),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Future<String?> getAccessToken() async {
|
|
63
|
+
return _storage.read(key: _Keys.accessToken);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Future<String?> getRefreshToken() async {
|
|
67
|
+
return _storage.read(key: _Keys.refreshToken);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Future<void> deleteTokens() async {
|
|
71
|
+
await Future.wait([
|
|
72
|
+
_storage.delete(key: _Keys.accessToken),
|
|
73
|
+
_storage.delete(key: _Keys.refreshToken),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// 로그아웃 시 모든 민감 데이터 삭제
|
|
78
|
+
Future<void> clearAll() async {
|
|
79
|
+
await _storage.deleteAll();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
abstract class _Keys {
|
|
84
|
+
static const accessToken = 'access_token';
|
|
85
|
+
static const refreshToken = 'refresh_token';
|
|
86
|
+
static const userPin = 'user_pin_hash';
|
|
87
|
+
static const biometricEnabled = 'biometric_enabled';
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Android 설정
|
|
92
|
+
|
|
93
|
+
```kotlin
|
|
94
|
+
// android/app/build.gradle
|
|
95
|
+
android {
|
|
96
|
+
defaultConfig {
|
|
97
|
+
minSdk 23 // EncryptedSharedPreferences 최소 요구
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```xml
|
|
103
|
+
<!-- AndroidManifest.xml (선택: 백업에서 Keystore 제외) -->
|
|
104
|
+
<application
|
|
105
|
+
android:fullBackupContent="@xml/backup_rules"
|
|
106
|
+
android:dataExtractionRules="@xml/data_extraction_rules">
|
|
107
|
+
</application>
|
|
108
|
+
|
|
109
|
+
<!-- res/xml/backup_rules.xml -->
|
|
110
|
+
<!-- <exclude domain="sharedpref" path="FlutterSecureStorage"/> -->
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### iOS 설정
|
|
114
|
+
|
|
115
|
+
```dart
|
|
116
|
+
// Keychain Accessibility 옵션
|
|
117
|
+
const iOptions = IOSOptions(
|
|
118
|
+
// 기기 잠금 해제 후 접근 가능 (기기 바인딩)
|
|
119
|
+
accessibility: KeychainAccessibility.first_unlock_this_device,
|
|
120
|
+
// iCloud Keychain 동기화 비활성화 (기기 전용)
|
|
121
|
+
synchronizable: false,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Keychain Accessibility 레벨:
|
|
125
|
+
// - first_unlock_this_device: 기기 첫 잠금 해제 후 접근 (권장)
|
|
126
|
+
// - unlocked_this_device: 잠금 해제 상태에서만 접근 (더 엄격)
|
|
127
|
+
// - first_unlock: iCloud 동기화 가능 (덜 안전)
|
|
128
|
+
// - passcode: 패스코드 설정된 기기에서만
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Riverpod Provider 구성
|
|
132
|
+
|
|
133
|
+
```dart
|
|
134
|
+
final secureStorageServiceProvider = Provider<SecureStorageService>((ref) {
|
|
135
|
+
return SecureStorageService();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/// 토큰 존재 여부 (인증 상태 판단)
|
|
139
|
+
final hasValidTokenProvider = FutureProvider<bool>((ref) async {
|
|
140
|
+
final storage = ref.watch(secureStorageServiceProvider);
|
|
141
|
+
final token = await storage.getAccessToken();
|
|
142
|
+
return token != null;
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 저장해야 할 것 vs 하지 말아야 할 것
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
✅ flutter_secure_storage 저장 대상:
|
|
150
|
+
- Access Token / Refresh Token
|
|
151
|
+
- API 시크릿 (런타임 주입된)
|
|
152
|
+
- 사용자 PIN 해시
|
|
153
|
+
- 생체 인증 활성화 플래그
|
|
154
|
+
- 암호화 키 (DB 암호화용)
|
|
155
|
+
|
|
156
|
+
❌ flutter_secure_storage에 저장하지 말 것:
|
|
157
|
+
- 대량 데이터 (성능 저하) → 암호화된 DB 사용
|
|
158
|
+
- 사용자 설정 (테마, 언어) → SharedPreferences OK
|
|
159
|
+
- 캐시 데이터 → 일반 파일 시스템
|
|
160
|
+
- 검색이 필요한 데이터 → DB 사용
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 규칙
|
|
164
|
+
|
|
165
|
+
- 토큰/시크릿 → `flutter_secure_storage` 필수 (SharedPreferences 금지)
|
|
166
|
+
- Android → `encryptedSharedPreferences: true` 설정 (AES-256 암호화)
|
|
167
|
+
- iOS → `KeychainAccessibility.first_unlock_this_device` (기기 바인딩)
|
|
168
|
+
- iOS → `synchronizable: false` (iCloud 동기화 비활성화)
|
|
169
|
+
- 로그아웃 시 → `deleteAll()` 로 모든 민감 데이터 삭제
|
|
170
|
+
- 대량 데이터 → secure_storage 대신 암호화된 DB (SQLCipher) 사용
|
|
171
|
+
- 앱 재설치 시 → Android Keystore 자동 초기화, iOS Keychain 잔존 가능 → 첫 실행 시 정리 로직
|