설정 A 분야 10: 암호 보관함 (VaultSettings) - 키 3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
056ca186d9
commit
a35293271e
|
|
@ -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<void> shot(String name, Size size, Directory dir) async {
|
||||
|
|
|
|||
|
|
@ -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<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run];
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault];
|
||||
|
||||
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
||||
Map<String, Object?> 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
|
||||
|
|
|
|||
|
|
@ -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<String, Object?> j, {required bool plainSecrets}) {
|
||||
j['vaultEnabled'] = enabled;
|
||||
j['vaultMasterCopy'] = masterCopy;
|
||||
j['vaultAutoLockSeconds'] = autoLockSeconds;
|
||||
}
|
||||
|
||||
@override
|
||||
void read(Map<String, dynamic> j) {
|
||||
enabled = j['vaultEnabled'] == true;
|
||||
masterCopy = j['vaultMasterCopy'] == true;
|
||||
autoLockSeconds = j['vaultAutoLockSeconds'] is num ? (j['vaultAutoLockSeconds'] as num).round().clamp(0, 3600) : 60;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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('기본 앱으로 열기')),
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ Future<void> 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('암호 보관함은 초기화되지 않습니다 - 보관함 파일은 그대로 남고 보관함 비밀번호로만 열 수 있습니다. [마스터로도 열기] 를 켰다면 마스터를 초기화할 때 그 길만 없어집니다'),
|
||||
|
|
|
|||
|
|
@ -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<int>(
|
||||
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!),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -330,6 +330,6 @@ Future<void> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class _VaultPageState extends State<VaultPage> 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<VaultPage> 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<VaultPage> 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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 <String>[];
|
||||
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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue