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
|
|
@ -75,12 +75,14 @@ void main() {
|
|||
List<int> bytes, {
|
||||
String? as,
|
||||
Set<Scope> scopes = const {},
|
||||
bool? encrypted,
|
||||
}) async {
|
||||
final hash = as ?? sha256.convert(bytes).toString();
|
||||
final description = await endpoints.asset.startUpload(
|
||||
signedIn(person, scopes: scopes),
|
||||
hash,
|
||||
bytes.length,
|
||||
encrypted: encrypted,
|
||||
);
|
||||
if (description != null) {
|
||||
await _upload(session, description, bytes);
|
||||
|
|
@ -88,6 +90,7 @@ void main() {
|
|||
return await endpoints.asset.finishUpload(
|
||||
signedIn(person, scopes: scopes),
|
||||
hash,
|
||||
encrypted: encrypted,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +186,48 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'an encrypted file (ADR 0019) over 20 MB is kept, as nothing can '
|
||||
'look into it, a little larger than 50 MB too, and no larger',
|
||||
() async {
|
||||
const size = AssetEndpoint.maxOtherBytes + 1;
|
||||
final sealed = Uint8List(size)..fillRange(0, size, 7);
|
||||
final readable = Uint8List(size)..fillRange(0, size, 8);
|
||||
const most =
|
||||
AssetEndpoint.maxBytes + AssetEndpoint.encryptionOverhead;
|
||||
|
||||
expect(await add(alice, sealed, encrypted: true), isTrue);
|
||||
// A readable file is still checked to be a PDF.
|
||||
expect(await add(alice, readable, encrypted: false), isFalse);
|
||||
expect(
|
||||
await endpoints.asset.startUpload(
|
||||
signedIn(alice),
|
||||
imageHash,
|
||||
most,
|
||||
encrypted: true,
|
||||
),
|
||||
isNotNull,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.asset.startUpload(
|
||||
signedIn(alice),
|
||||
imageHash,
|
||||
most + 1,
|
||||
encrypted: true,
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.asset.startUpload(
|
||||
signedIn(alice),
|
||||
imageHash,
|
||||
AssetEndpoint.maxBytes + 1,
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('finishes no upload that never came', () async {
|
||||
expect(
|
||||
await endpoints.asset.finishUpload(signedIn(alice), imageHash),
|
||||
|
|
|
|||
Loading…
Reference in a new issue