feat(privacy): every synced payload sealed and opened through one envelope (ADR 0019)
- `ContentKeys` opens the person's data keys (`content_key`) with their account's keys on the device, an anonymous person's device keys made as their data first opens; it makes a private studyset's key and the personal key. Without the account's keys nothing opens and nothing new is private (D8). - `PayloadEnvelope` is the one place payloads are sealed and opened, each for its table, row and column, so a payload moved to another row never opens; one that doesn't open is `Locked`, never half-read. - `Privacy` gives each gateway the key of the row's tree (its top-level studyset's), and rewrites a tree under another key, or readable, in place (`rekeyTree`). - The studyset, card, note, learning path, path answer and PDF gateways seal with their tree's key and open what they read: a new top-level studyset is private where the device holds the keys, a new one inside a tree takes its privacy, and one moved into another tree is rewritten under that tree's key. Tags and settings are sealed with the personal key; a tag or settings row that doesn't open here is left out, so nothing folds or deletes it. A studyset whose key doesn't open shows as locked (`Studyset.isLocked`); other rows that don't open read as blank. - Every payload codec round-trips sealed, and refuses another row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
905bb9abf2
commit
64fdfa091d
1 changed files with 1693 additions and 151 deletions
|
|
@ -1,11 +1,15 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_file_cache.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/serverpod_asset_server_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/sync_asset_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/privacy/data/content_keys.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/privacy/data/privacy.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/privacy/data/sync_content_key_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/sync_card_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
||||
|
|
@ -183,14 +187,29 @@ final class SyncDevice(
|
|||
),
|
||||
debounce: const Duration(days: 1),
|
||||
);
|
||||
// Without the account's keys, as before private studysets: what these
|
||||
// tests sync stays readable, which their SQL checks rely on.
|
||||
final privacy = Privacy(
|
||||
db,
|
||||
ContentKeys(
|
||||
SyncContentKeyGateway(db, syncRequests),
|
||||
FakeContentCipher(),
|
||||
() async => null,
|
||||
),
|
||||
);
|
||||
final cards = CardRepository(
|
||||
SyncCardGateway(db, syncRequests, person.authUserId.uuid),
|
||||
SyncCardGateway(db, syncRequests, person.authUserId.uuid, privacy),
|
||||
);
|
||||
final notes = NoteRepository(
|
||||
SyncNoteGateway(db, syncRequests, person.authUserId.uuid),
|
||||
SyncNoteGateway(db, syncRequests, person.authUserId.uuid, privacy),
|
||||
);
|
||||
final paths = LearningPathRepository(
|
||||
SyncLearningPathGateway(db, syncRequests, person.authUserId.uuid),
|
||||
SyncLearningPathGateway(
|
||||
db,
|
||||
syncRequests,
|
||||
person.authUserId.uuid,
|
||||
privacy,
|
||||
),
|
||||
);
|
||||
final assets = AssetRepository(
|
||||
SyncAssetGateway(db, syncRequests),
|
||||
|
|
@ -201,21 +220,24 @@ final class SyncDevice(
|
|||
client,
|
||||
db,
|
||||
syncRequests,
|
||||
StudysetRepository(SyncStudysetGateway(db, syncRequests)),
|
||||
StudysetRepository(SyncStudysetGateway(db, syncRequests, privacy)),
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
PathProgressRepository(SyncPathAnswerGateway(db, syncRequests)),
|
||||
PathProgressRepository(SyncPathAnswerGateway(db, syncRequests, privacy)),
|
||||
ReviewRepository(SyncReviewGateway(db, syncRequests)),
|
||||
TagRepository(
|
||||
SyncTagGateway(db, syncRequests),
|
||||
SyncTagGateway(db, syncRequests, privacy),
|
||||
cardChanges: cards.changes,
|
||||
noteChanges: notes.changes,
|
||||
pathChanges: paths.changes,
|
||||
),
|
||||
SettingsRepository(SyncSettingsGateway(db, syncRequests)),
|
||||
SettingsRepository(SyncSettingsGateway(db, syncRequests, privacy)),
|
||||
assets,
|
||||
PdfSourceRepository(SyncPdfSourceGateway(db, syncRequests), assets),
|
||||
PdfSourceRepository(
|
||||
SyncPdfSourceGateway(db, syncRequests, privacy),
|
||||
assets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue