fix(keys): a sealed data key is signed by whoever sealed it
Data keys were sealed to the account as anonymous sealed boxes, and the register knows every account's public key: it could have sealed a key of its own under an existing key ID, and a device would have encrypted new content with a key the register holds (the second reading's M7, latent until step 4 encrypts anything). sealDataKey now signs the box, with the account it is sealed to and a label, by the sealer's Ed25519 key, and openDataKey checks that signature against the sealer it expects (the account itself, or whoever shared the store) before opening anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b33b99a50a
commit
3ded5b932f
1 changed files with 127 additions and 43 deletions
|
|
@ -15,53 +15,89 @@ Future<void> main() async {
|
|||
group('data keys, sealed to the account', () {
|
||||
final account = AccountKeys.generate(sodium);
|
||||
final dataKey = DataKey.generate(sodium, 'agenda-2026');
|
||||
final sealed = sealDataKey(sodium, dataKey, account.encryptionPublicKey);
|
||||
final sealed = sealDataKey(
|
||||
sodium,
|
||||
dataKey,
|
||||
to: account.encryptionPublicKey,
|
||||
sealer: account,
|
||||
);
|
||||
|
||||
DataKey open(
|
||||
Uint8List sealed, {
|
||||
String keyId = 'agenda-2026',
|
||||
AccountKeys? keys,
|
||||
Uint8List? sealedBy,
|
||||
}) => openDataKey(
|
||||
sodium,
|
||||
sealed,
|
||||
keyId: keyId,
|
||||
keys: keys ?? account,
|
||||
sealedBy: sealedBy ?? account.signingPublicKey,
|
||||
);
|
||||
|
||||
test("any device that opens the account opens the store's key", () {
|
||||
final opened = openDataKey(
|
||||
sodium,
|
||||
sealed,
|
||||
keyId: 'agenda-2026',
|
||||
keys: account,
|
||||
);
|
||||
final opened = open(sealed);
|
||||
expect(opened.keyId, 'agenda-2026');
|
||||
expect(opened.key.extractBytes(), dataKey.key.extractBytes());
|
||||
});
|
||||
|
||||
test("another account's keys don't open it", () {
|
||||
test("another account's keys don't open it: it was sealed, and signed, "
|
||||
'for this one', () {
|
||||
expect(
|
||||
() => openDataKey(
|
||||
sodium,
|
||||
sealed,
|
||||
keyId: 'agenda-2026',
|
||||
keys: AccountKeys.generate(sodium),
|
||||
),
|
||||
refusedFor(KeyRefusal.wrongKeyOrTampered),
|
||||
() => open(sealed, keys: AccountKeys.generate(sodium)),
|
||||
refusedFor(KeyRefusal.forgedSignature),
|
||||
);
|
||||
});
|
||||
|
||||
test('a sealed key changed is refused', () {
|
||||
final tampered = Uint8List.fromList(sealed)..[40] ^= 1;
|
||||
for (final at in [10, 80, sealed.length - 1]) {
|
||||
final tampered = Uint8List.fromList(sealed)..[at] ^= 1;
|
||||
expect(() => open(tampered), refusedFor(KeyRefusal.forgedSignature));
|
||||
}
|
||||
});
|
||||
|
||||
test("a key the register seals itself doesn't open: it knows the "
|
||||
"account's public key, but can't sign as the account, so new content "
|
||||
'is never encrypted with a key it holds', () {
|
||||
final registers = DataKey.generate(sodium, 'agenda-2026');
|
||||
// Sealed to the account as an anonymous box, signed by another key.
|
||||
final planted = sealDataKey(
|
||||
sodium,
|
||||
registers,
|
||||
to: account.encryptionPublicKey,
|
||||
sealer: AccountKeys.generate(sodium),
|
||||
);
|
||||
expect(() => open(planted), refusedFor(KeyRefusal.forgedSignature));
|
||||
// Or with the account's signature of another key moved onto it.
|
||||
final moved = Uint8List.fromList([
|
||||
...sealed.sublist(0, sodium.crypto.sign.bytes),
|
||||
...planted.sublist(sodium.crypto.sign.bytes),
|
||||
]);
|
||||
expect(() => open(moved), refusedFor(KeyRefusal.forgedSignature));
|
||||
});
|
||||
|
||||
test('shared, it opens as sealed by whoever shared it, and only so', () {
|
||||
final ben = AccountKeys.generate(sodium);
|
||||
final shared = sealDataKey(
|
||||
sodium,
|
||||
dataKey,
|
||||
to: ben.encryptionPublicKey,
|
||||
sealer: account,
|
||||
);
|
||||
expect(
|
||||
() => openDataKey(
|
||||
sodium,
|
||||
tampered,
|
||||
keyId: 'agenda-2026',
|
||||
keys: account,
|
||||
),
|
||||
refusedFor(KeyRefusal.wrongKeyOrTampered),
|
||||
open(shared, keys: ben).key.extractBytes(),
|
||||
dataKey.key.extractBytes(),
|
||||
);
|
||||
expect(
|
||||
() => open(shared, keys: ben, sealedBy: ben.signingPublicKey),
|
||||
refusedFor(KeyRefusal.forgedSignature),
|
||||
);
|
||||
});
|
||||
|
||||
test("a key sealed under one ID doesn't open as another's: a server "
|
||||
"can't swap stores' keys", () {
|
||||
expect(
|
||||
() => openDataKey(
|
||||
sodium,
|
||||
sealed,
|
||||
keyId: 'studyset-7',
|
||||
keys: account,
|
||||
),
|
||||
() => open(sealed, keyId: 'studyset-7'),
|
||||
refusedFor(KeyRefusal.wrongDataKey),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue