diff --git a/lib/core/vault_store.dart b/lib/core/vault_store.dart index a45fa85..41d3c41 100644 --- a/lib/core/vault_store.dart +++ b/lib/core/vault_store.dart @@ -49,7 +49,8 @@ class Vault { final Set _folders; Vault._(this.dir, this._key, this._entries, [Set? folders]) : _folders = folders ?? {}; - static const format = 1; + /// 1 = 1단계, 2 = 2단계 (보관함 안 폴더 - 관리자 지시 10/21: 1단계만 아는 판은 "새 판으로" 를 알리게) + static const format = 2; static const _json = 'vault.json', _index = 'index.jjv', _ext = '.jjv', _folderFile = 'folders.jjd'; List get entries => List.unmodifiable(_entries); @@ -101,18 +102,21 @@ class Vault { final iterations = (j['iterations'] as num).toInt(); final kek = await Isolate.run(() => VaultCrypto.kek(password, salt, iterations)); final key = VaultCrypto.unwrapKey(kek, base64.decode('${j['nonce']}'), base64.decode('${j['key']}')); - return _open(dir, key); + return _open(dir, key, j); } /// 안전 저장소의 열쇠로 연다 ([마스터로도 열기]) static Future unlockWithKey(Directory dir, Uint8List key) async { - await _readHeader(dir); - return _open(dir, key); + return _open(dir, key, await _readHeader(dir)); } - static Future _open(Directory dir, Uint8List key) async { + static Future _open(Directory dir, Uint8List key, Map header) async { final data = await File(p.join(dir.path, _index)).readAsBytes(); final list = jsonDecode(utf8.decode(VaultCrypto.decryptAll(key, data))) as List; + // 1단계 보관함은 열쇠가 맞은 뒤 형식 번호만 올린다 (감싼 열쇠 · 솔트는 그대로) + if ((header['format'] as int) < format) { + await _atomicWrite(File(p.join(dir.path, _json)), utf8.encode(const JsonEncoder.withIndent(' ').convert({...header, 'format': format}))); + } final folders = {}; final ff = File(p.join(dir.path, _folderFile)); if (await ff.exists()) { diff --git a/test/vault_test.dart b/test/vault_test.dart index 6f7a7b3..997d403 100644 --- a/test/vault_test.dart +++ b/test/vault_test.dart @@ -195,7 +195,7 @@ void main() { expect(dir.listSync().length, 2, reason: 'vault.json · index.jjv 만'); await expectLater(Vault.create(dir, 'x', iterations: 1000), throwsA(isA())); final j = File(p.join(dir.path, 'vault.json')); - j.writeAsStringSync(j.readAsStringSync().replaceFirst('"format": 1', '"format": 2')); + j.writeAsStringSync(j.readAsStringSync().replaceFirst('"format": 2', '"format": 3')); await expectLater(Vault.unlock(dir, 'pw'), throwsA(isA())); }); @@ -262,6 +262,16 @@ void main() { File(p.join(dir.path, 'index.jjv')).writeAsBytesSync(VaultCrypto.encryptAll(key, Uint8List.fromList(utf8.encode(jsonEncode(bad))))); final again = await Vault.unlock(dir, 'pw'); expect(again.entries.last.folder, ''); + // 1단계 (형식 1) 보관함: 열리고, 열쇠가 맞은 뒤 형식 번호만 2 로 (1단계만 아는 판은 "새 판으로" 를 알림) + final j = File(p.join(dir.path, 'vault.json')); + final before = jsonDecode(j.readAsStringSync()) as Map; + j.writeAsStringSync(j.readAsStringSync().replaceFirst('"format": 2', '"format": 1')); + await expectLater(Vault.unlock(dir, 'wrong'), throwsA(isA())); + expect((jsonDecode(j.readAsStringSync()) as Map)['format'], 1, reason: '틀린 비밀번호면 바꾸지 않음'); + await Vault.unlock(dir, 'pw'); + final after = jsonDecode(j.readAsStringSync()) as Map; + expect(after['format'], 2); + expect([after['salt'], after['key'], after['nonce']], [before['salt'], before['key'], before['nonce']]); }); }); }