Merge branch 'secret_test' into next
commit
49990c4120
|
|
@ -1,14 +1,29 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||
|
||||
/// 54: 이 기기의 안전 저장소 (Windows DPAPI · Android Keystore) 에 실제로 쓰고 읽고 지운다.
|
||||
/// 시험용 열쇠 하나만 쓰고 끝에 지운다 (사용자가 저장한 값은 건드리지 않음).
|
||||
/// Windows 는 임시 폴더의 secrets.dat 만 쓴다 (관리자 지시 10/17 - 사용자의 앱 데이터 폴더 secrets.dat 는 건드리지 않음,
|
||||
/// 같은 시험이 보통 회귀에도 있음: test/secret_store_dpapi_test.dart). Android 는 기기의 Keystore 를 써야 하므로 그대로.
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
Directory? tmp;
|
||||
SecretStore store() {
|
||||
if (!Platform.isWindows) return SecretStore.platform();
|
||||
return SecretStore.dpapiAt((tmp ??= Directory.systemTemp.createTempSync('jj_secretit_')).path);
|
||||
}
|
||||
|
||||
tearDownAll(() {
|
||||
try {
|
||||
tmp?.deleteSync(recursive: true);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
testWidgets('안전 저장소: 쓰기 · 읽기 · 지우기', (t) async {
|
||||
final s = SecretStore.platform();
|
||||
final s = store();
|
||||
const key = 'jj.selftest';
|
||||
await s.write(key, '비밀-1');
|
||||
expect((await s.readAll())[key], '비밀-1');
|
||||
|
|
@ -20,12 +35,12 @@ void main() {
|
|||
|
||||
testWidgets('130: 두 창 (저장소 두 개) 이 번갈아 써도 서로의 값을 지우지 않는다', (t) async {
|
||||
const a = 'jj.selftest.a', b = 'jj.selftest.b';
|
||||
final first = SecretStore.platform(), second = SecretStore.platform();
|
||||
final first = store(), second = store();
|
||||
try {
|
||||
await first.readAll(); // 첫 창이 읽어 둠
|
||||
await second.write(b, '두 번째 창'); // 두 번째 창이 씀
|
||||
await first.write(a, '첫 창'); // 첫 창이 쓸 때 두 번째 창의 값을 지우면 안 된다
|
||||
final now = await SecretStore.platform().readAll();
|
||||
final now = await store().readAll();
|
||||
expect(now[a], '첫 창');
|
||||
expect(now[b], '두 번째 창');
|
||||
await second.delete(b); // 두 번째 창이 지울 때 첫 창의 값을 되살리거나 지우면 안 된다
|
||||
|
|
@ -33,10 +48,10 @@ void main() {
|
|||
expect(after[a], '첫 창');
|
||||
expect(after.containsKey(b), isFalse);
|
||||
} finally {
|
||||
await SecretStore.platform().delete(a);
|
||||
await SecretStore.platform().delete(b);
|
||||
await store().delete(a);
|
||||
await store().delete(b);
|
||||
}
|
||||
final end = await SecretStore.platform().readAll();
|
||||
final end = await store().readAll();
|
||||
expect(end.containsKey(a) || end.containsKey(b), isFalse);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:ffi';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -19,6 +20,10 @@ abstract class SecretStore {
|
|||
/// 저장소를 풀 수 없어 새로 시작했으면 풀지 못한 원래 파일을 보관한 곳 (켤 때 알림 - 40-5)
|
||||
String? get lostCopy => null;
|
||||
|
||||
/// 시험: Windows DPAPI 저장소를 [dir] 의 secrets.dat 로 (사용자의 앱 데이터 폴더 secrets.dat 를 건드리지 않게 - 관리자 지시 10/17)
|
||||
@visibleForTesting
|
||||
factory SecretStore.dpapiAt(String dir) => _DpapiSecretStore(dir: dir);
|
||||
|
||||
/// 이 기기의 안전 저장소
|
||||
factory SecretStore.platform() {
|
||||
if (Platform.isWindows) return _DpapiSecretStore();
|
||||
|
|
@ -48,7 +53,11 @@ class _AndroidSecretStore implements SecretStore {
|
|||
|
||||
/// Windows: 모든 값을 JSON 으로 묶어 DPAPI (CryptProtectData, 지금 사용자 범위) 로 암호화한 secrets.dat
|
||||
class _DpapiSecretStore implements SecretStore {
|
||||
Future<File> _file() async => File(p.join((await getApplicationSupportDirectory()).path, 'secrets.dat'));
|
||||
/// 둘 폴더 (null 이면 앱 데이터 폴더 - 시험만 따로 정한다)
|
||||
final String? dir;
|
||||
_DpapiSecretStore({this.dir});
|
||||
|
||||
Future<File> _file() async => File(p.join(dir ?? (await getApplicationSupportDirectory()).path, 'secrets.dat'));
|
||||
|
||||
Map<String, String>? _cache;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/services/secret_store.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 54 · 130 (관리자 지시 10/17): Windows 안전 저장소 (DPAPI) 를 실제로 쓰고 읽고 지운다 - 보통 회귀 시험에 들어가게.
|
||||
/// 임시 폴더의 secrets.dat 만 쓴다 (사용자의 앱 데이터 폴더 secrets.dat 는 건드리지 않음 - integration_test 의 예전 시험은 그것을 썼다).
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
final userFile = Platform.isWindows
|
||||
? File(p.join(Platform.environment['APPDATA'] ?? '', 'com.jj', 'jj_mkvmaker', 'secrets.dat'))
|
||||
: null;
|
||||
(DateTime, int)? userBefore;
|
||||
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_secret_');
|
||||
final f = userFile;
|
||||
userBefore = f != null && f.existsSync() ? (f.lastModifiedSync(), f.lengthSync()) : null;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
// 사용자의 secrets.dat 는 그대로여야 한다 (있으면 바뀐 때 · 크기가 같음)
|
||||
final f = userFile;
|
||||
if (f != null && userBefore != null) {
|
||||
expect((f.lastModifiedSync(), f.lengthSync()), userBefore, reason: '사용자 secrets.dat 를 건드리면 안 됨');
|
||||
}
|
||||
try {
|
||||
tmp.deleteSync(recursive: true);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
test('쓰기 · 읽기 · 바꾸기 · 지우기 - 파일은 암호화되어 평문이 없다', () async {
|
||||
final s = SecretStore.dpapiAt(tmp.path);
|
||||
expect(await s.readAll(), isEmpty);
|
||||
await s.write('jj.selftest', '비밀-1');
|
||||
expect((await s.readAll())['jj.selftest'], '비밀-1');
|
||||
final file = File(p.join(tmp.path, 'secrets.dat'));
|
||||
expect(file.existsSync(), isTrue);
|
||||
final raw = file.readAsBytesSync();
|
||||
expect(String.fromCharCodes(raw).contains('jj.selftest'), isFalse, reason: 'DPAPI 로 암호화 - 열쇠 이름도 보이지 않음');
|
||||
await s.write('jj.selftest', '비밀-2');
|
||||
expect((await SecretStore.dpapiAt(tmp.path).readAll())['jj.selftest'], '비밀-2', reason: '새로 연 저장소도 같은 값');
|
||||
await s.delete('jj.selftest');
|
||||
expect((await s.readAll()).containsKey('jj.selftest'), isFalse);
|
||||
expect(File('${file.path}.tmp').existsSync(), isFalse, reason: '임시 파일이 남지 않음');
|
||||
}, skip: Platform.isWindows ? false : 'Windows 만 (DPAPI)');
|
||||
|
||||
test('130: 두 창 (저장소 두 개) 이 번갈아 써도 서로의 값을 지우지 않는다', () async {
|
||||
const a = 'jj.selftest.a', b = 'jj.selftest.b';
|
||||
final first = SecretStore.dpapiAt(tmp.path), second = SecretStore.dpapiAt(tmp.path);
|
||||
await first.readAll(); // 첫 창이 읽어 둠
|
||||
await second.write(b, '두 번째 창');
|
||||
await first.write(a, '첫 창'); // 첫 창이 쓸 때 두 번째 창의 값을 지우면 안 된다
|
||||
final now = await SecretStore.dpapiAt(tmp.path).readAll();
|
||||
expect(now[a], '첫 창');
|
||||
expect(now[b], '두 번째 창');
|
||||
await second.delete(b);
|
||||
final after = await first.readAll();
|
||||
expect(after[a], '첫 창');
|
||||
expect(after.containsKey(b), isFalse);
|
||||
}, skip: Platform.isWindows ? false : 'Windows 만 (DPAPI)');
|
||||
|
||||
test('풀 수 없는 파일 (다른 PC · 사용자에서 옮겨 옴) 은 지우지 않고 보관한 뒤 새로 시작', () async {
|
||||
final file = File(p.join(tmp.path, 'secrets.dat'))..writeAsBytesSync([1, 2, 3, 4, 5]);
|
||||
final s = SecretStore.dpapiAt(tmp.path);
|
||||
expect(await s.readAll(), isEmpty);
|
||||
expect(s.lostCopy, isNotNull);
|
||||
expect(File(s.lostCopy!).readAsBytesSync(), [1, 2, 3, 4, 5]);
|
||||
expect(p.basename(s.lostCopy!), startsWith('secrets.broken-'));
|
||||
expect(file.readAsBytesSync(), [1, 2, 3, 4, 5], reason: '새로 쓰기 전에는 원래 파일 그대로');
|
||||
}, skip: Platform.isWindows ? false : 'Windows 만 (DPAPI)');
|
||||
}
|
||||
Loading…
Reference in New Issue