ADR 0017's step 1. Generic over group and hash (SrpGroup); production takes RFC 5054's 3072-bit group with SHA-256. k = H(N | PAD(g)), u = H(PAD(A) | PAD(B)), K = H(PAD(S)), M1 = H(H(N) xor H(g) | H(I) | s | PAD(A) | PAD(B) | K), M2 = H(PAD(A) | M1 | K). A and B must lie in 1..N-1 and u must not be 0; a and b are 256 bits from Random.secure(); proofs are compared in constant time. x comes from the caller (halkyon_keys makes it); RFC 5054's own x is there for the vectors only. - The register's half in halkyon_base_server (base.dart: srpChallenge, srpVerify), the device's written by hand in halkyon_base_client (srp.dart: SrpClient, srpVerifier). Serverpod's generator cleans and writes only a client's lib/src/protocol/ (serverpod_cli 4.0.4's _getDirectoriesRequiringCleaning; a generate leaves srp/ untouched). - Tests: RFC 5054 Appendix B on both halves; the client against the register, succeeding and every refusal. - The module's tests depend on its client, and pub resolves every member's dev dependencies, so Learn's server image takes the module's client into its workspace too. - The format check covers the hand-written half. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
188 lines
6.3 KiB
Dart
188 lines
6.3 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:halkyon_base_client/src/srp/srp_math.dart';
|
|
import 'package:halkyon_base_client/srp.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/// [hex] as RFC 5054 prints it (in groups, across lines) as a number.
|
|
BigInt _hex(String hex) =>
|
|
BigInt.parse(hex.replaceAll(RegExp(r'\s'), ''), radix: 16);
|
|
|
|
void main() {
|
|
group("RFC 5054's test vectors (Appendix B), value by value", () {
|
|
final group = SrpGroup.rfc5054TestVectorGroup;
|
|
final salt = bytesOf(_hex('BEB25379 D1A8581E B5A72767 3A2441EE'));
|
|
final identity = utf8.encode('alice');
|
|
final privateValue = rfc5054PrivateValue(
|
|
group,
|
|
salt: salt,
|
|
identity: 'alice',
|
|
password: 'password123',
|
|
);
|
|
final client = SrpClient.withSecret(
|
|
group,
|
|
_hex('''
|
|
60975527 035CF2AD 1989806F 0407210B C81EDC04 E2762A56 AFD529DD
|
|
DA2D4393'''),
|
|
);
|
|
final serverValue = _hex('''
|
|
BD0C6151 2C692C0C B6D041FA 01BB152D 4916A1E7 7AF46AE1 05393011
|
|
BAF38964 DC46A067 0DD125B9 5A981652 236F99D9 B681CBF8 7837EC99
|
|
6C6DA044 53728610 D0C6DDB5 8B318885 D7D82C7F 8DEB75CE 7BD4FBAA
|
|
37089E6F 9C6059F3 88838E7A 00030B33 1EB76840 910440B1 B27AAEAE
|
|
EB4012B7 D7665238 A8E3FB00 4B117B58''');
|
|
|
|
test('k = H(N | PAD(g))', () {
|
|
expect(
|
|
multiplierOf(group),
|
|
_hex('7556AA04 5AEF2CDD 07ABAF0F 665C3E81 8913186F'),
|
|
);
|
|
});
|
|
|
|
test('x = H(s | H(I | ":" | P))', () {
|
|
expect(
|
|
privateValue,
|
|
_hex('94B7555A ABE9127C C58CCF49 93DB6CF8 4D16C124'),
|
|
);
|
|
});
|
|
|
|
test('v = g^x mod N', () {
|
|
expect(
|
|
srpVerifier(group, privateValue),
|
|
_hex('''
|
|
7E273DE8 696FFC4F 4E337D05 B4B375BE B0DDE156 9E8FA00A 9886D812
|
|
9BADA1F1 822223CA 1A605B53 0E379BA4 729FDC59 F105B478 7E5186F5
|
|
C671085A 1447B52A 48CF1970 B4FB6F84 00BBF4CE BFBB1681 52E08AB5
|
|
EA53D15C 1AFF87B2 B9DA6E04 E058AD51 CC72BFC9 033B564E 26480D78
|
|
E955A5E2 9E7AB245 DB2BE315 E2099AFB'''),
|
|
);
|
|
});
|
|
|
|
test('A = g^a mod N', () {
|
|
expect(
|
|
client.publicValue,
|
|
_hex('''
|
|
61D5E490 F6F1B795 47B0704C 436F523D D0E560F0 C64115BB 72557EC4
|
|
4352E890 3211C046 92272D8B 2D1A5358 A2CF1B6E 0BFCF99F 921530EC
|
|
8E393561 79EAE45E 42BA92AE ACED8251 71E1E8B9 AF6D9C03 E1327F44
|
|
BE087EF0 6530E69F 66615261 EEF54073 CA11CF58 58F0EDFD FE15EFEA
|
|
B349EF5D 76988A36 72FAC47B 0769447B'''),
|
|
);
|
|
});
|
|
|
|
test('u = H(PAD(A) | PAD(B))', () {
|
|
expect(
|
|
scramblerOf(group, client.publicValue, serverValue),
|
|
_hex('CE38B959 3487DA98 554ED47D 70A7AE5F 462EF019'),
|
|
);
|
|
});
|
|
|
|
test('the premaster secret S = (B - k·g^x)^(a + u·x) mod N', () {
|
|
expect(
|
|
client.premasterSecretOf(
|
|
privateValue: privateValue,
|
|
serverValue: serverValue,
|
|
scrambler: scramblerOf(group, client.publicValue, serverValue),
|
|
),
|
|
_hex('''
|
|
B0DC82BA BCF30674 AE450C02 87745E79 90A3381F 63B387AA F271A10D
|
|
233861E3 59B48220 F7C4693C 9AE12B0A 6F67809F 0876E2D0 13800D6C
|
|
41BB59B6 D5979B5C 00A172B4 A2A5903A 0BDCAF8A 709585EB 2AFAFA8F
|
|
3499B200 210DCC1F 10EB3394 3CD67FC8 8A2F39A4 BE5BEC4E C0A3212D
|
|
C346D7E4 74B29EDE 8A469FFE CA686E5A'''),
|
|
);
|
|
});
|
|
|
|
test('the proof is made from those values', () {
|
|
final step = client.respond(
|
|
identity: identity,
|
|
salt: salt,
|
|
privateValue: privateValue,
|
|
serverValue: serverValue,
|
|
);
|
|
expect(step, isA<SrpClientProof>());
|
|
final proof = step as SrpClientProof;
|
|
// SHA-1 as the vectors' group has it: 20 bytes each.
|
|
expect(proof.proof, hasLength(20));
|
|
expect(proof.sessionKey, hasLength(20));
|
|
});
|
|
});
|
|
|
|
group("the production group, RFC 5054's 3072-bit with SHA-256", () {
|
|
final group = SrpGroup.rfc5054Group3072;
|
|
|
|
test('is the 3072-bit safe prime with the generator 5', () {
|
|
expect(group.n.bitLength, 3072);
|
|
expect(group.length, 384);
|
|
expect(group.g, BigInt.from(5));
|
|
// RFC 3526's MODP group 15: it starts and ends with 64 ones.
|
|
expect(group.n >> 3008, (BigInt.one << 64) - BigInt.one);
|
|
expect(
|
|
group.n & ((BigInt.one << 64) - BigInt.one),
|
|
(BigInt.one << 64) - BigInt.one,
|
|
);
|
|
});
|
|
|
|
test('every client takes a fresh secret, so a fresh A', () {
|
|
final first = SrpClient(group).publicValue;
|
|
final second = SrpClient(group).publicValue;
|
|
expect(first, isNot(second));
|
|
expect(first > BigInt.one && first < group.n, isTrue);
|
|
});
|
|
});
|
|
|
|
group("a register's B the client refuses, which would give the proof "
|
|
'away', () {
|
|
final group = SrpGroup.rfc5054Group3072;
|
|
final client = SrpClient(group);
|
|
SrpClientStep respondTo(BigInt serverValue) => client.respond(
|
|
identity: utf8.encode('someone'),
|
|
salt: List.filled(16, 7),
|
|
privateValue: BigInt.from(1234567),
|
|
serverValue: serverValue,
|
|
);
|
|
|
|
for (final (name, value) in [
|
|
('zero', BigInt.zero),
|
|
('N', group.n),
|
|
('a multiple of N', group.n * BigInt.two),
|
|
('above N', group.n + BigInt.one),
|
|
('negative', -BigInt.one),
|
|
]) {
|
|
test('B $name', () {
|
|
expect(
|
|
respondTo(value),
|
|
isA<SrpClientRefused>().having(
|
|
(refused) => refused.reason,
|
|
'reason',
|
|
SrpClientRefusal.invalidServerValue,
|
|
),
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('the bytes', () {
|
|
test('a number takes its fewest bytes, zero one', () {
|
|
expect(bytesOf(BigInt.zero), [0]);
|
|
expect(bytesOf(BigInt.from(255)), [255]);
|
|
expect(bytesOf(BigInt.from(256)), [1, 0]);
|
|
expect(
|
|
numberOf(bytesOf(BigInt.parse('123456789012345678901234567890'))),
|
|
BigInt.parse('123456789012345678901234567890'),
|
|
);
|
|
});
|
|
|
|
test('PAD fills with leading zeros, and refuses what is too long', () {
|
|
expect(padded(BigInt.from(5), 4), [0, 0, 0, 5]);
|
|
expect(() => padded(BigInt.from(1 << 16), 2), throwsArgumentError);
|
|
});
|
|
|
|
test('proofs compare in constant time, unequal lengths unequal', () {
|
|
expect(constantTimeEquals([1, 2, 3], [1, 2, 3]), isTrue);
|
|
expect(constantTimeEquals([1, 2, 3], [1, 2, 4]), isFalse);
|
|
expect(constantTimeEquals([1, 2, 3], [1, 2]), isFalse);
|
|
expect(constantTimeEquals(<int>[], <int>[]), isTrue);
|
|
});
|
|
});
|
|
}
|