halkyon/packages/halkyon_app/test/files/file_saver_test.dart
Tizian 「ティツィアーン」 801d0c6679
fix(app): a saved file is the own user's from the start, and replaces the one chosen whole
The reading of the full reading's fixes found that writePrivately made
the file, then set it to 0600: another local user could open it in that
window and keep reading what was written after; and that where the path
was a link or another user's file, chmod failed and the secret was
written into it anyway.

On Linux and macOS it now makes a new file beside the one chosen, 0600
as it is created (O_EXCL, O_NOFOLLOW), writes through the descriptor that
made it, and renames it over the chosen path, so the old file is never
written into; a symbolic link or another user's file is refused, with
nothing written (lstat through FFI, by each system's layout). Windows
writes in place, with its folder's rights.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 14:13:00 +02:00

126 lines
4.5 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:path/path.dart' as p;
void main() {
late Directory folder;
setUp(() async {
folder = await Directory.systemTemp.createTemp('file_saver_test');
});
tearDown(() => folder.delete(recursive: true));
test("a saved file is the own user's alone where the platform has modes, "
'set before anything is written', () async {
final path = p.join(folder.path, 'halkyon-recovery-codes.txt');
// One there before, readable by everyone.
await File(path).writeAsString('old');
if (!Platform.isWindows) {
await Process.run('chmod', ['644', path]);
}
await writePrivately(path, Uint8List.fromList([1, 2, 3]));
expect(await File(path).readAsBytes(), [1, 2, 3]);
if (Platform.isLinux || Platform.isMacOS) {
expect(File(path).statSync().mode & 0x1ff, 0x180, reason: '0600');
}
}, skip: Platform.isWindows ? 'no modes' : false);
final hasModes = Platform.isLinux || Platform.isMacOS;
test('a saved file replaces the one there without writing into it: '
'whoever held the old file open, or a link to it, never reads what is '
'saved (the reading of the fixes)', () async {
final path = p.join(folder.path, 'halkyon-recovery-codes.txt');
await File(path).writeAsString('old');
await Process.run('chmod', ['644', path]);
// Another name for the same file, as a descriptor opened earlier is.
final elsewhere = p.join(folder.path, 'held-open');
await Process.run('ln', [path, elsewhere]);
await writePrivately(path, Uint8List.fromList(utf8.encode('secret')));
expect(await File(path).readAsString(), 'secret');
expect(File(path).statSync().mode & 0x1ff, 0x180, reason: '0600');
expect(await File(elsewhere).readAsString(), 'old');
// Nothing left beside it.
expect(
folder.listSync().map((entry) => p.basename(entry.path)).toSet(),
{'halkyon-recovery-codes.txt', 'held-open'},
);
}, skip: hasModes ? false : 'no modes');
test('never follows a symbolic link where the file is to be saved: '
'refused, the file it points to untouched', () async {
final target = p.join(folder.path, 'someone-elses.txt');
await File(target).writeAsString('theirs');
final path = p.join(folder.path, 'halkyon-recovery-phrase.txt');
await Link(path).create(target);
await expectLater(
writePrivately(path, Uint8List.fromList(utf8.encode('secret'))),
throwsA(isA<FileSystemException>()),
);
expect(await File(target).readAsString(), 'theirs');
expect(FileSystemEntity.isLinkSync(path), isTrue);
expect(
folder.listSync().map((entry) => p.basename(entry.path)).toSet(),
{'someone-elses.txt', 'halkyon-recovery-phrase.txt'},
);
}, skip: hasModes ? false : 'no modes');
test(
"refuses a file another user owns, writing nothing: a root's file "
'stands in for one in a shared folder',
() async {
const rootsFile = '/etc/hostname';
final before = File(rootsFile).readAsStringSync();
await expectLater(
writePrivately(rootsFile, Uint8List.fromList([1])),
throwsA(
isA<FileSystemException>().having(
(error) => error.message,
'message',
"Another user's file",
),
),
);
expect(File(rootsFile).readAsStringSync(), before);
},
skip: !hasModes || !File('/etc/hostname').existsSync()
? 'no such file'
: false,
);
test('a new file is made 0600 from the start, with nothing left beside '
'it', () async {
final path = p.join(folder.path, 'new.txt');
await writePrivately(path, Uint8List.fromList([4, 5, 6]));
expect(await File(path).readAsBytes(), [4, 5, 6]);
expect(File(path).statSync().mode & 0x1ff, 0x180, reason: '0600');
expect(folder.listSync(), hasLength(1));
}, skip: hasModes ? false : 'no modes');
test("the system picker's copy in the temporary folder is removed, "
'others left', () async {
final copy = File(p.join(folder.path, 'halkyon-recovery-phrase.txt'));
final other = File(p.join(folder.path, 'something-else.txt'));
await copy.writeAsString('the phrase');
await other.writeAsString('other');
await forgetPickerCopy('halkyon-recovery-phrase.txt', temporary: folder);
await forgetPickerCopy('not-there.txt', temporary: folder);
expect(copy.existsSync(), isFalse);
expect(other.existsSync(), isTrue);
});
}