mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-15 09:20:48 +01:00
CLI caches the E2E key (0600)
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Argon2id ran on every CLI/MCP call; the derived key is now cached owner-only in the data dir. Todo: fda71ed1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f7e24bbb1a
commit
19f930a93a
@ -7,6 +7,7 @@
|
||||
/// records hand-written into RTDB.
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crdt_sync/crdt_sync.dart';
|
||||
@ -149,6 +150,8 @@ class CliSession {
|
||||
store: _remote(push: push),
|
||||
oldPrefix: plainService.notesDir,
|
||||
readPassphrase: _readPassphrase,
|
||||
// Argon2id once per install, not on every CLI/MCP call.
|
||||
keyCache: E2eKeyCache(read: _readCachedKey, write: _writeCachedKey),
|
||||
);
|
||||
if (!push) {
|
||||
// Read-only during the migration: old builds still write the plaintext
|
||||
@ -181,6 +184,29 @@ class CliSession {
|
||||
RemoteStore _remote({required bool push}) =>
|
||||
push ? _firebase : PullOnlyStore(_firebase);
|
||||
|
||||
/// Owner-only file in the data dir caching the derived E2E key.
|
||||
static const e2eKeyFileName = 'e2e_key';
|
||||
|
||||
File get _keyFile => File(p.join(dataDir, e2eKeyFileName));
|
||||
|
||||
Future<List<int>?> _readCachedKey() async {
|
||||
final file = _keyFile;
|
||||
if (!file.existsSync()) return null;
|
||||
try {
|
||||
return base64.decode((await file.readAsString()).trim());
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeCachedKey(List<int> bytes) async {
|
||||
final file = _keyFile;
|
||||
// Restrict before the key is written, never after.
|
||||
await file.writeAsString('');
|
||||
await Process.run('chmod', ['600', file.path]);
|
||||
await file.writeAsString(base64.encode(bytes), flush: true);
|
||||
}
|
||||
|
||||
/// Environment variable holding the shared crdt-sync passphrase.
|
||||
static const e2ePassphraseEnv = 'TODO_E2E_PASSPHRASE';
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.86+1
|
||||
version: 1.0.87+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
@ -129,6 +129,21 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('the derived key is cached owner-only for the next run', () async {
|
||||
final first = await open('cache');
|
||||
await first.repository.upsert(note('n1'));
|
||||
await first.sync(push: true);
|
||||
await first.close();
|
||||
final cached = File('${tmp.path}/cache/${CliSession.e2eKeyFileName}');
|
||||
expect(cached.existsSync(), isTrue);
|
||||
expect(cached.statSync().mode & 0x1ff, 384); // 0600
|
||||
// Without any passphrase the next run still unlocks from the cache.
|
||||
File('${tmp.path}/e2e_passphrase').deleteSync();
|
||||
final second = await open('cache');
|
||||
expect((await second.sync(push: true)).pushed, isFalse);
|
||||
await second.close();
|
||||
});
|
||||
|
||||
test('a second node sees the first node note', () async {
|
||||
final a = await open('a');
|
||||
await a.repository.upsert(note('shared'));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user