fix(auth): decoy encryption keys are keys a device could have made

An address nobody has got, as the recovery phrase's first step, an
X25519 public key of 32 HMAC bytes, which is a point of Curve25519's
prime-order subgroup one time in sixteen, as every real key is: whoever
checked could tell nearly always whether an address has an account (the
second reading's M1). The decoy's key is now made from a seed, as its
Ed25519 key already was. The tests hold twenty decoys to the curve check
the review used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-20 21:04:50 +02:00
commit 009970faab
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -6,6 +6,8 @@ import 'package:halkyon_base_server/src/password/key_proof.dart';
import 'package:serverpod/serverpod.dart';
import 'package:test/test.dart';
import '../../integration/support/curve25519.dart';
Uint8List _hex(String hex) => Uint8List.fromList([
for (var i = 0; i < hex.length; i += 2)
int.parse(hex.substring(i, i + 2), radix: 16),
@ -147,5 +149,29 @@ void main() {
)).extractPublicKey()).bytes,
);
});
test(
'decoy X25519 keys from a seed are points of the prime-order '
"subgroup, as devices' keys are, and random bytes mostly aren't",
() async {
for (var i = 0; i < 20; i++) {
final seed = List.generate(32, (j) => (i * 31 + j * 17) % 256);
final decoy = await x25519PublicKeyFromSeed(seed);
expect(await x25519PublicKeyFromSeed(seed), decoy);
expect(isX25519PublicKey(decoy), isTrue);
final made = await (await X25519().newKeyPair()).extractPublicKey();
expect(isX25519PublicKey(made.bytes), isTrue);
}
// The check itself: of byte strings that aren't keys, few pass.
var passing = 0;
for (var i = 0; i < 64; i++) {
final bytes = List.generate(32, (j) => (i * 131 + j * 29 + 7) % 256);
if (isX25519PublicKey(bytes)) {
passing++;
}
}
expect(passing, lessThan(16));
},
);
});
}