feat(privacy): the instance's side of private studysets (ADR 0019)
- `content_key`, a synced table in the person's space, keeps their data keys sealed to their account (a private studyset's, their personal one); `learning_path_answer` gains `keyId` and `nonce` for answers in private studysets. Additive only, on the server and the device. - Publishing refuses a private studyset, and a tree holding a private row (`PublishRefusal.private`); a private studyset elsewhere no longer breaks publishing a readable one. Encrypted tags are named as the device names them (`tagNames`): a name for a tag that isn't the person's, or an encrypted tag of the release without one, refuses it (`tagNamesMismatch`); a readable tag keeps its own name. - The connector leaves private studysets out and says how many there are. - The learner count reads no payload any more: Count me is kept by the instance (`counted_learner`) and the device reports which publications its copies come from (`learning_copy`, accepted only while counted, each report replacing the last); only the reporter's own studysets count. Deleting a person deletes both. - An upload says whether its file is encrypted: only a readable one over 20 MB is checked to be a PDF, and an encrypted one may be 4 KiB larger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
074d05ab4e
commit
a1c5ac3f86
1 changed files with 35261 additions and 198 deletions
|
|
@ -549,7 +549,10 @@ void main() {
|
|||
final second = jsonDecode(renewed.body) as Map;
|
||||
expect(second['access_token'], isNot(first['access_token']));
|
||||
expect(reused.status, 400);
|
||||
expect(await call(second['access_token'], 'list_studysets'), '[]');
|
||||
expect(
|
||||
jsonDecode(await call(second['access_token'], 'list_studysets')),
|
||||
{'studysets': <Object?>[]},
|
||||
);
|
||||
expect(
|
||||
(await mcp(first['access_token'], {
|
||||
'jsonrpc': '2.0',
|
||||
|
|
@ -609,17 +612,23 @@ void main() {
|
|||
List<(String, String)> cards = const [],
|
||||
UuidValue? parent,
|
||||
DateTime? deletedAt,
|
||||
bool private = false,
|
||||
}) => asPerson(person, (transaction) async {
|
||||
final id = (await Studyset.db.insertRow(
|
||||
session,
|
||||
Studyset(
|
||||
parentId: parent,
|
||||
deletedAt: deletedAt,
|
||||
payload: _json({
|
||||
'schemaVersion': 4,
|
||||
'title': title,
|
||||
'description': _rich('About $title'),
|
||||
}),
|
||||
// A private studyset's payload is ciphertext (ADR 0019).
|
||||
keyId: private ? 'studyset-key' : null,
|
||||
nonce: private ? ByteData(24) : null,
|
||||
payload: private
|
||||
? ByteData.sublistView(Uint8List.fromList([0xff, 0xfe, 7]))
|
||||
: _json({
|
||||
'schemaVersion': 4,
|
||||
'title': title,
|
||||
'description': _rich('About $title'),
|
||||
}),
|
||||
),
|
||||
transaction: transaction,
|
||||
)).id!;
|
||||
|
|
@ -740,6 +749,35 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
test('leaves out private studysets, saying how many, and reads '
|
||||
'nothing in them', () async {
|
||||
final diary = await studyset(
|
||||
alice,
|
||||
'Diary',
|
||||
cards: [('secret', 'kept')],
|
||||
private: true,
|
||||
);
|
||||
await studyset(alice, 'Inside', parent: diary, private: true);
|
||||
await studyset(alice, 'Greek');
|
||||
final token = (await tokensOf(alice))['access_token'];
|
||||
|
||||
final listed = jsonDecode(await call(token, 'list_studysets')) as Map;
|
||||
expect(
|
||||
[for (final s in listed['studysets'] as List) (s as Map)['title']],
|
||||
['Greek'],
|
||||
);
|
||||
expect(listed['private_studysets'], 1);
|
||||
expect(listed['private_note'], contains('end-to-end encrypted'));
|
||||
expect(
|
||||
await call(token, 'read_studyset', {'studyset_id': diary.uuid}),
|
||||
contains('end-to-end encrypted'),
|
||||
);
|
||||
final found = jsonDecode(
|
||||
await call(token, 'search_cards', {'query': 'kept'}),
|
||||
) as List;
|
||||
expect(found, isEmpty);
|
||||
});
|
||||
|
||||
test('leaves out a studyset waiting in Recently deleted, and what '
|
||||
'is inside it', () async {
|
||||
final japanese = await studyset(
|
||||
|
|
@ -753,7 +791,9 @@ void main() {
|
|||
final token = (await tokensOf(alice))['access_token'];
|
||||
|
||||
final listed =
|
||||
jsonDecode(await call(token, 'list_studysets')) as List;
|
||||
(jsonDecode(await call(token, 'list_studysets'))
|
||||
as Map)['studysets']
|
||||
as List;
|
||||
expect([for (final s in listed) (s as Map)['title']], ['Greek']);
|
||||
final found = jsonDecode(
|
||||
await call(token, 'search_cards', {'query': 'fire'}),
|
||||
|
|
@ -773,7 +813,9 @@ void main() {
|
|||
final token = (await tokensOf(alice))['access_token'];
|
||||
|
||||
final listed =
|
||||
jsonDecode(await call(token, 'list_studysets')) as List;
|
||||
(jsonDecode(await call(token, 'list_studysets'))
|
||||
as Map)['studysets']
|
||||
as List;
|
||||
expect(
|
||||
[for (final s in listed) (s as Map)['title']],
|
||||
[
|
||||
|
|
|
|||
Loading…
Reference in a new issue