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>
This commit is contained in:
parent
999877d95c
commit
801d0c6679
1 changed files with 302 additions and 25 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
|
|
@ -30,6 +31,85 @@ void main() {
|
|||
}
|
||||
}, 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'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue