From 54eb692001a5561b15cb9f620afa3746d5fe2fdf Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:09:18 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=ED=86=B5=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=9D=80=20=EC=9D=B4=20=ED=8C=90=EC=9D=B4=20=EB=A7=8C=EB=93=9C?= =?UTF-8?q?=EB=8A=94=20=EA=BC=B4=EC=9D=98=20=EB=B9=84=EB=B0=80=20=EC=97=B4?= =?UTF-8?q?=EC=87=A0=EB=A7=8C=20=EC=A7=80=EC=9A=B4=EB=8B=A4=20-=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EB=A8=B8=EB=A6=AC=EA=B8=80=EC=9D=98=20?= =?UTF-8?q?=EB=AA=A8=EB=A5=B4=EB=8A=94=20=EA=BC=B4=20(=EC=83=88=20?= =?UTF-8?q?=ED=8C=90=EC=9D=98=20dav.token.x=20=EB=93=B1)=20=EC=9D=80=20?= =?UTF-8?q?=EB=82=A8=EA=B9=80=20(=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A7=80?= =?UTF-8?q?=EC=8B=9C=2010/21,=2015=20=EC=95=8C=EB=A6=BC,=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 틈: _writeSecrets 가 머리글 (os. · dav. · ai. · sftp. · smb. · ftp.) 만 보고 지금 설정에 없는 열쇠를 지웠다. 새 판이 같은 머리글로 새 종류의 열쇠를 쓰면 예전 판으로 되돌려 저장할 때 사용자 비밀번호를 잃을 수 있었다 (40 의 비밀 값 쪽) - SettingsStore.ownsSecret 을 정확한 꼴로: os.key · os.user · os.password · dav.servers · dav.pw. · sftp.pw|key|kp. · smb.pw. · ftp.pw. · ai.key. ([_secretsOf] 가 만드는 것과 같음). 지우는 것은 이 꼴 중 그 서버 · 항목이 설정에서 없어지거나 비운 것만 - 초기화의 [저장된 실제 비밀번호 · API 키] (clearForeignSecrets) 는 그대로 sec. 밖을 모두 지움 - 시험 settings_store_test: 모르는 꼴 (dav.token.d1 · sftp.cert.s1 · nfs.pw.n1) 은 보통 저장 · 다시 켜 저장 뒤에도 남음, 서버 s1 을 지우면 s1 의 pw · key · kp 만 없어지고 s2 · 모르는 꼴 · sec. 은 그대로, clearForeignSecrets 는 3개 지우고 sec. 만 남김. 관련 9파일 81개 통과 Co-Authored-By: Claude Opus 5.5 --- lib/app/settings.dart | 11 +++++++---- test/settings_store_test.dart | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/app/settings.dart b/lib/app/settings.dart index aed7a6c..48c0514 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -1682,11 +1682,14 @@ class SettingsStore { }); } - /// 이 판의 설정이 맡는 안전 저장소 열쇠 (그 밖은 마스터 · 최상 (sec.) 이나 이 판이 모르는 것 - 새 판이 쓴 열쇠) - static bool ownsSecret(String k) => - k.startsWith('os.') || k.startsWith('dav.') || k.startsWith('ai.') || k.startsWith('sftp.') || k.startsWith('smb.') || k.startsWith('ftp.'); + /// 이 판의 설정이 맡는 안전 저장소 열쇠 - 이 판이 만드는 정확한 이름 꼴만 ([_secretsOf] 와 같은 꼴). + /// 관리자 지시 (10/21, 15 알림): 예전에는 머리글 (dav. · sftp. 등) 만 보아, 새 판이 같은 머리글로 새 종류의 열쇠 (예: dav.token.x) 를 + /// 쓰면 이 판에서 보통 저장할 때 지웠다 - 예전 판으로 되돌리면 사용자 비밀번호를 잃을 수 있었다 (40 의 비밀 값 쪽). + /// 그 밖은 마스터 · 최상 (sec.) 이거나 이 판이 모르는 꼴 (다른 판이 쓴 열쇠) - 보통 저장에서는 건드리지 않는다 + static bool ownsSecret(String k) => _osKeys.containsValue(k) || k == _davServers || _ownForm.hasMatch(k); + static final _ownForm = RegExp(r'^(dav\.pw|sftp\.pw|sftp\.key|sftp\.kp|smb\.pw|ftp\.pw|ai\.key)\..+$'); - /// 관리자 판단 (10/21): 이 판이 모르는 비밀 값 (다른 판이 쓴 열쇠) 을 지운다 - 사용자가 초기화 창에서 + /// 관리자 판단 (10/21): 이 판이 모르는 꼴의 비밀 값 (다른 판이 쓴 열쇠 - 머리글이 같아도) 을 지운다 - 사용자가 초기화 창에서 /// [저장된 실제 비밀번호 · API 키] 를 골랐을 때만 부른다 (고르지 않았으면 남김). 마스터 · 최상 (sec.) 은 [마스터 비밀번호] 체크가 정해 여기서 남긴다. /// 지운 열쇠 수 Future clearForeignSecrets() async { diff --git a/test/settings_store_test.dart b/test/settings_store_test.dart index de2c96a..13038bc 100644 --- a/test/settings_store_test.dart +++ b/test/settings_store_test.dart @@ -2,6 +2,9 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/settings.dart'; +import 'package:jj_mkvmaker/core/sftp.dart'; +import 'package:jj_mkvmaker/core/webdav.dart'; +import 'package:jj_mkvmaker/services/secret_store.dart'; import 'package:path/path.dart' as p; void main() { @@ -25,6 +28,40 @@ void main() { expect((await SettingsStore(file).load()).homeUrl, 'https://4.example'); }); + test('관리자 지시 (10/21): 보통 저장은 이 판이 만드는 꼴의 열쇠만 지운다 - 모르는 꼴 (새 판의 dav.token.x 등) 은 남김 · 지운 서버의 아는 열쇠만', () async { + final tmp = Directory.systemTemp.createTempSync('jj_secret_form_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + final secrets = MemorySecretStore() + ..values['dav.token.d1'] = 'new-kind' // 새 판이 같은 머리글로 쓴 새 종류 + ..values['sftp.cert.s1'] = 'new-kind-2' + ..values['nfs.pw.n1'] = 'new-server-kind' + ..values['sec.master'] = 'hash'; + final store = SettingsStore(p.join(tmp.path, 'settings.json'), secrets); + final s = await store.load() + ..webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')] + ..sftpServers = [ + const SftpServer(id: 's1', name: 'a', host: 'h', user: 'u', password: 'sp1', privateKey: 'KEY1', keyPassphrase: 'kp1'), + const SftpServer(id: 's2', name: 'b', host: 'h', user: 'u', password: 'sp2'), + ]; + await store.save(s); + expect( + {for (final k in ['dav.pw.d1', 'sftp.pw.s1', 'sftp.key.s1', 'sftp.kp.s1', 'sftp.pw.s2']) k: secrets.values[k]}, + {'dav.pw.d1': 'dp', 'sftp.pw.s1': 'sp1', 'sftp.key.s1': 'KEY1', 'sftp.kp.s1': 'kp1', 'sftp.pw.s2': 'sp2'}); + // 서버 s1 을 지움: s1 의 아는 열쇠 (pw · key · kp) 만 없어지고, s2 · 모르는 꼴 · 마스터는 그대로 + s.sftpServers = [s.sftpServers.last]; + await store.save(s); + expect(secrets.values.keys.where((k) => k.endsWith('.s1')), ['sftp.cert.s1']); + expect(secrets.values['sftp.pw.s2'], 'sp2'); + expect((secrets.values['dav.token.d1'], secrets.values['nfs.pw.n1'], secrets.values['sec.master']), ('new-kind', 'new-server-kind', 'hash')); + // 다시 읽어 저장해도 (새 SettingsStore = 앱을 다시 켬) 모르는 꼴은 남음 + final again = SettingsStore(p.join(tmp.path, 'settings.json'), secrets); + await again.save(await again.load()); + expect((secrets.values['dav.token.d1'], secrets.values['sftp.cert.s1']), ('new-kind', 'new-kind-2')); + // 초기화의 [저장된 실제 비밀번호 · API 키] 쪽 (clearForeignSecrets) 은 모르는 꼴도 지우고 sec. 만 남김 + expect(await again.clearForeignSecrets(), 3); + expect(secrets.values.keys.where((k) => !SettingsStore.ownsSecret(k)), ['sec.master']); + }); + test('깨진 설정 파일: 백업으로 되살리고, 깨진 파일은 보관 · 알림 정보', () async { final store = SettingsStore(file); await store.save(AppSettings()..homeUrl = 'https://good.example');