From a35293271e59aff57a80c83b16ef45bd1b67641d Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:30:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=A4=EC=A0=95=20A=20=EB=B6=84=EC=95=BC=201?= =?UTF-8?q?0:=20=EC=95=94=ED=98=B8=20=EB=B3=B4=EA=B4=80=ED=95=A8=20(VaultS?= =?UTF-8?q?ettings)=20-=20=ED=82=A4=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- integration_test/windows_vault_shot_test.dart | 2 +- lib/app/settings.dart | 20 ++++--------- lib/app/settings/vault_settings.dart | 30 +++++++++++++++++++ lib/ui/explorer/explorer_buttons.dart | 2 +- lib/ui/explorer/item_menu.dart | 2 +- lib/ui/master_prompt.dart | 2 +- lib/ui/security_settings.dart | 10 +++---- lib/ui/vault/vault_actions.dart | 2 +- lib/ui/vault_page.dart | 8 ++--- test/vault_page_test.dart | 8 ++--- 10 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 lib/app/settings/vault_settings.dart diff --git a/integration_test/windows_vault_shot_test.dart b/integration_test/windows_vault_shot_test.dart index 12ffd1f..4787056 100644 --- a/integration_test/windows_vault_shot_test.dart +++ b/integration_test/windows_vault_shot_test.dart @@ -26,7 +26,7 @@ void main() { final tmp = Directory.systemTemp.createTempSync('jj_vaultshot_'); addTearDown(() => tmp.deleteSync(recursive: true)); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.vaultEnabled = true; + c.settings.vault.enabled = true; final key = GlobalKey(); Future shot(String name, Size size, Directory dir) async { diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 6591ead..f6b3059 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -15,6 +15,7 @@ import '../services/app_shell.dart' show appIconOf; import '../core/remote_idle.dart'; import 'settings/records.dart'; import 'settings/settings_group.dart'; +import 'settings/vault_settings.dart'; import 'settings/run_settings.dart'; import 'settings/browser_settings.dart'; import 'settings/download_settings.dart'; @@ -28,6 +29,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/vault_settings.dart'; export 'settings/run_settings.dart'; export 'settings/browser_settings.dart'; export 'settings/download_settings.dart'; @@ -156,9 +158,6 @@ class AppSettings { - /// X-plore 비교 5-2 암호 보관함 (사용자 결정 10/21, 관리자 결정): 처음 끔. 보관함 자체는 설정이 아니라 그 폴더 (vault.json · *.jjv) 에 있다 - - /// 설정을 처음 상태로 돌려도 보관함은 그대로 (끄기만 됨) - bool vaultEnabled = false; /// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용, 사용자 결정 10/21 · 관리자 결정: 처음 끔 · 켤 때마다 경고) bool shizukuEnabled = false; @@ -166,11 +165,7 @@ class AppSettings { /// X-plore 비교 5-2 루트 폴더 보기 (su, 읽기 전용, 관리자 결정 10/21: Shizuku 와 같은 틀 - 처음 끔 · 켤 때 · 처음 열 때 경고) bool rootEnabled = false; - /// [마스터로도 열기] - 보관함 열쇠 한 벌을 안전 저장소 (마스터 비밀번호로 막힘) 에 둔다. 처음 끔. 마스터를 초기화하면 그 한 벌은 지워진다 - bool vaultMasterCopy = false; - /// 앱이 뒤로 간 뒤 이만큼 (초) 지나면 다시 잠금 (처음 60) - int vaultAutoLockSeconds = 60; @@ -361,8 +356,11 @@ class AppSettings { /// 실행 · 종료 · 업데이트 final run = RunSettings(); + /// 암호 보관함 + final vault = VaultSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run]; + List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -404,11 +402,8 @@ class AppSettings { 'wifiShareMinutes': wifiShareMinutes, 'wifiShareTailscale': wifiShareTailscale, 'wifiShareFolder': wifiShareFolder, - 'vaultEnabled': vaultEnabled, 'shizukuEnabled': shizukuEnabled, 'rootEnabled': rootEnabled, - 'vaultMasterCopy': vaultMasterCopy, - 'vaultAutoLockSeconds': vaultAutoLockSeconds, 'mkvExisting': mkvExisting, 'components': components, 'aiImageV1': true, @@ -497,11 +492,8 @@ class AppSettings { ? j['screenOrientation'] as String : 'auto' ..openFileWindow = j['openFileWindow'] == 'new' ? 'new' : 'same' - ..vaultEnabled = j['vaultEnabled'] == true ..shizukuEnabled = j['shizukuEnabled'] == true ..rootEnabled = j['rootEnabled'] == true - ..vaultMasterCopy = j['vaultMasterCopy'] == true - ..vaultAutoLockSeconds = j['vaultAutoLockSeconds'] is num ? (j['vaultAutoLockSeconds'] as num).round().clamp(0, 3600) : 60 ..zipLevel = const [1, 6, 9].contains(j['zipLevel']) ? j['zipLevel'] as int : 6 ..wifiShareMinutes = const [0, 10, 30, 60, 120].contains(j['wifiShareMinutes']) ? j['wifiShareMinutes'] as int : 30 ..wifiShareTailscale = j['wifiShareTailscale'] is bool ? j['wifiShareTailscale'] as bool : true diff --git a/lib/app/settings/vault_settings.dart b/lib/app/settings/vault_settings.dart new file mode 100644 index 0000000..d78c016 --- /dev/null +++ b/lib/app/settings/vault_settings.dart @@ -0,0 +1,30 @@ + +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 암호 보관함 - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class VaultSettings implements SettingsGroup { + /// X-plore 비교 5-2 암호 보관함 (사용자 결정 10/21, 관리자 결정): 처음 끔. 보관함 자체는 설정이 아니라 그 폴더 (vault.json · *.jjv) 에 있다 - + /// 설정을 처음 상태로 돌려도 보관함은 그대로 (끄기만 됨) + bool enabled = false; + + /// [마스터로도 열기] - 보관함 열쇠 한 벌을 안전 저장소 (마스터 비밀번호로 막힘) 에 둔다. 처음 끔. 마스터를 초기화하면 그 한 벌은 지워진다 + bool masterCopy = false; + + /// 앱이 뒤로 간 뒤 이만큼 (초) 지나면 다시 잠금 (처음 60) + int autoLockSeconds = 60; + + @override + void write(Map j, {required bool plainSecrets}) { + j['vaultEnabled'] = enabled; + j['vaultMasterCopy'] = masterCopy; + j['vaultAutoLockSeconds'] = autoLockSeconds; + } + + @override + void read(Map j) { + enabled = j['vaultEnabled'] == true; + masterCopy = j['vaultMasterCopy'] == true; + autoLockSeconds = j['vaultAutoLockSeconds'] is num ? (j['vaultAutoLockSeconds'] as num).round().clamp(0, 3600) : 60; + } +} diff --git a/lib/ui/explorer/explorer_buttons.dart b/lib/ui/explorer/explorer_buttons.dart index 27e2f48..72abbf1 100644 --- a/lib/ui/explorer/explorer_buttons.dart +++ b/lib/ui/explorer/explorer_buttons.dart @@ -215,7 +215,7 @@ class ExplorerButtons { case ExplorerButton.apps: await openAppManager(cmd.context); case ExplorerButton.vault: - if (!ctl.c.settings.vaultEnabled) { + if (!ctl.c.settings.vault.enabled) { ctl.host.snack(tr('암호 보관함은 환경 설정 > 보안 에서 켭니다 (처음 꺼짐)')); } else { await openVault(cmd.context, ctl.c); diff --git a/lib/ui/explorer/item_menu.dart b/lib/ui/explorer/item_menu.dart index 1dc0f6d..3def05b 100644 --- a/lib/ui/explorer/item_menu.dart +++ b/lib/ui/explorer/item_menu.dart @@ -106,7 +106,7 @@ class ExplorerItemMenu { // X-plore 비교 5-1 ④ (관리자 지시 10/21): SQLite (읽기 전용) if (!e.isDir && !inArchive && sqliteExtensions.contains(ext)) ('sqlite', Icons.table_chart_outlined, tr('SQLite 보기 (읽기 전용)')), // X-plore 비교 5-2 (사용자 결정 10/21): 암호 보관함에 넣기. 2단계 (관리자 지시 10/21): 폴더째 (안의 구조 그대로) · 원격 파일도 (받아서 넣음) - if (ctl.c.settings.vaultEnabled && !inArchive) + if (ctl.c.settings.vault.enabled && !inArchive) ('vaultAdd', Icons.enhanced_encryption_outlined, many ? trf('{0}개 보관함에 넣기', [targets.length]) : tr('보관함에 넣기')), if (!e.isDir) ('openWith', Icons.open_in_new, tr('다른 앱으로 열기')), if (!e.isDir && !video) ('openDefault', Icons.launch, tr('기본 앱으로 열기')), diff --git a/lib/ui/master_prompt.dart b/lib/ui/master_prompt.dart index 8bfc0d4..6776522 100644 --- a/lib/ui/master_prompt.dart +++ b/lib/ui/master_prompt.dart @@ -217,7 +217,7 @@ Future showSuperReset(BuildContext context, AppController c, MasterLock lo '${tr('다른 판에서 만든 설정도 지웁니다 (다른 판이 넣은 비밀번호는 [저장된 실제 비밀번호 · API 키] 를 함께 골라야 지워집니다).')}'), ), // X-plore 비교 5-2 (관리자 결정 10/21): 보관함은 초기화 대상이 아님 (풀 수 있는 열쇠가 없음) - if (c.settings.vaultEnabled) + if (c.settings.vault.enabled) Padding( padding: const EdgeInsets.only(top: 6), child: Text(tr('암호 보관함은 초기화되지 않습니다 - 보관함 파일은 그대로 남고 보관함 비밀번호로만 열 수 있습니다. [마스터로도 열기] 를 켰다면 마스터를 초기화할 때 그 길만 없어집니다'), diff --git a/lib/ui/security_settings.dart b/lib/ui/security_settings.dart index 5807df2..296704b 100644 --- a/lib/ui/security_settings.dart +++ b/lib/ui/security_settings.dart @@ -125,9 +125,9 @@ class SecuritySettings extends StatelessWidget { leading: const Icon(Icons.enhanced_encryption_outlined), title: Text(tr('암호 보관함')), subtitle: Text(tr('파일을 AES-256 으로 암호화해 보관합니다 (이름까지 숨김). 보관함 비밀번호를 잊으면 되살릴 수 없습니다')), - trailing: Switch(value: c.settings.vaultEnabled, onChanged: (v) => c.updateSettings((x) => x.vaultEnabled = v)), + trailing: Switch(value: c.settings.vault.enabled, onChanged: (v) => c.updateSettings((x) => x.vault.enabled = v)), ), - if (c.settings.vaultEnabled) ...[ + if (c.settings.vault.enabled) ...[ ListTile( contentPadding: const EdgeInsets.only(left: 56, right: 16), title: Text(tr('보관함 열기')), @@ -139,12 +139,12 @@ class SecuritySettings extends StatelessWidget { title: Text(tr('보관함 자동 잠금')), subtitle: Text(tr('앱이 뒤로 간 뒤 이만큼 지나 돌아오면 다시 비밀번호를 묻습니다')), trailing: DropdownButton( - value: c.settings.vaultAutoLockSeconds, + value: c.settings.vault.autoLockSeconds, items: [ - for (final v in {0, 30, 60, 300, 900, c.settings.vaultAutoLockSeconds}.toList()..sort()) + for (final v in {0, 30, 60, 300, 900, c.settings.vault.autoLockSeconds}.toList()..sort()) DropdownMenuItem(value: v, child: Text(v == 0 ? tr('바로') : v == 60 ? tr('1분 (기본)') : v < 60 ? trf('{0}초', [v]) : trf('{0}분', [v ~/ 60]))), ], - onChanged: (v) => c.updateSettings((x) => x.vaultAutoLockSeconds = v!), + onChanged: (v) => c.updateSettings((x) => x.vault.autoLockSeconds = v!), ), ), ], diff --git a/lib/ui/vault/vault_actions.dart b/lib/ui/vault/vault_actions.dart index 9077bf1..ac9eb45 100644 --- a/lib/ui/vault/vault_actions.dart +++ b/lib/ui/vault/vault_actions.dart @@ -330,6 +330,6 @@ Future vaultToggleMasterCopy(BuildContext context, AppController c, VaultU } else { await lock.secrets.delete(MasterLock.vaultCopyKey); } - await c.updateSettings((x) => x.vaultMasterCopy = on); + await c.updateSettings((x) => x.vault.masterCopy = on); if (context.mounted) s.changed(); } diff --git a/lib/ui/vault_page.dart b/lib/ui/vault_page.dart index 995e9cd..cd3dcb4 100644 --- a/lib/ui/vault_page.dart +++ b/lib/ui/vault_page.dart @@ -104,7 +104,7 @@ class _VaultPageState extends State with WidgetsBindingObserver { } else if (state == AppLifecycleState.resumed) { final at = _hiddenAt; _hiddenAt = null; - if (at != null && _s.vault != null && DateTime.now().difference(at).inSeconds >= c.settings.vaultAutoLockSeconds) { + if (at != null && _s.vault != null && DateTime.now().difference(at).inSeconds >= c.settings.vault.autoLockSeconds) { setState(_lock); } } @@ -291,13 +291,13 @@ class _VaultPageState extends State with WidgetsBindingObserver { onSelected: (w) => switch (w) { 'mkdir' => vaultMakeFolder(context, _s), 'pw' => vaultChangePassword(context, _s, iterations: vaultIterationsForTest), - 'master' => vaultToggleMasterCopy(context, c, _s, !c.settings.vaultMasterCopy), + 'master' => vaultToggleMasterCopy(context, c, _s, !c.settings.vault.masterCopy), _ => null, }, itemBuilder: (_) => [ PopupMenuItem(value: 'mkdir', child: Text(tr('새 폴더'))), PopupMenuItem(value: 'pw', child: Text(tr('보관함 비밀번호 바꾸기'))), - CheckedPopupMenuItem(value: 'master', checked: c.settings.vaultMasterCopy, child: Text(tr('마스터로도 열기'))), + CheckedPopupMenuItem(value: 'master', checked: c.settings.vault.masterCopy, child: Text(tr('마스터로도 열기'))), ], ), ], @@ -322,7 +322,7 @@ class _VaultPageState extends State with WidgetsBindingObserver { pw: _pw, waiting: _waitUntil != null && DateTime.now().isBefore(_waitUntil!), error: _error, - masterCopy: c.settings.vaultMasterCopy, + masterCopy: c.settings.vault.masterCopy, onUnlock: _unlock, onUnlockWithMaster: _unlockWithMaster, ), diff --git a/test/vault_page_test.dart b/test/vault_page_test.dart index 5512840..d5bef84 100644 --- a/test/vault_page_test.dart +++ b/test/vault_page_test.dart @@ -48,7 +48,7 @@ void main() { AppController ctl() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.vaultEnabled = true; + c.settings.vault.enabled = true; return c; } @@ -170,7 +170,7 @@ void main() { await settle(t); expect(find.textContaining('마스터를 초기화하면 이 길만 없어지고'), findsOneWidget); await t.tap(find.widgetWithText(FilledButton, '켜기')); - await until(t, () => c.settings.vaultMasterCopy); + await until(t, () => c.settings.vault.masterCopy); expect((await t.runAsync(secrets.readAll))!.containsKey(MasterLock.vaultCopyKey), isTrue); // 잠그고 마스터로 열기 await t.tap(find.byTooltip('잠그기')); @@ -214,7 +214,7 @@ void main() { Directory(p.join(dir.path, 'sub')).createSync(); for (final on in [false, true]) { final c = ctl() - ..settings.vaultEnabled = on + ..settings.vault.enabled = on ..settings.explorer.paths = [dir.path, dir.path] ..settings.explorer.layout = 'split'; await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: c)))); @@ -287,7 +287,7 @@ void main() { viewRoot.existsSync() ? viewRoot.listSync(recursive: true).map((e) => e.path).toList() : const []; final photo = File(p.join(tmp.path, 'photo.png'))..writeAsBytesSync(img.encodePng(img.Image(width: 20, height: 10))); final clip = File(p.join(tmp.path, 'clip.mp4'))..writeAsBytesSync(List.filled(500, 1)); - final c = ctl()..settings.vaultAutoLockSeconds = 0; + final c = ctl()..settings.vault.autoLockSeconds = 0; await t.pumpWidget(MaterialApp(home: VaultPage(c: c, add: [photo.path, clip.path]))); await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty); await t.enterText(find.byType(TextField).at(0), 'pw');