feat(crypto): SRP-6a, the register's half and the device's
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>
This commit is contained in:
parent
ae0d88818b
commit
bc499713b8
9 changed files with 1369 additions and 27 deletions
|
|
@ -5,4 +5,9 @@ what a Halkyon app calls to sign in, to manage its account, and to learn an inst
|
|||
basics, under `client.base` in the app's own client. Never edited by hand; run
|
||||
`serverpod --no-analytics generate` in `halkyon_base_server` instead.
|
||||
|
||||
The one exception is SRP-6a's client half (ADR 0017), written by hand beside the
|
||||
generated code, which the generator leaves alone (it writes only `lib/src/protocol/`):
|
||||
`package:halkyon_base_client/srp.dart`, tested by `dart test` here. The module's README
|
||||
says how it pairs with the register's half.
|
||||
|
||||
Under the GNU Affero General Public License 3.0 or later.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
# Defines a default set of lint rules enforced for
|
||||
# projects at Google. For details and rationale,
|
||||
# see https://github.com/dart-lang/pedantic#enabled-lints.
|
||||
|
||||
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||
# Uncomment to specify additional rules.
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
# The workspace's lint rules (see ../../analysis_options.yaml) for what is written
|
||||
# here by hand (lib/srp.dart, lib/src/srp/); the code Serverpod generates is left out.
|
||||
include: ../../analysis_options.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export 'src/protocol/protocol.dart';
|
||||
export 'package:serverpod_client/serverpod_client.dart';
|
||||
|
||||
export 'src/protocol/protocol.dart';
|
||||
|
|
|
|||
142
packages/halkyon_base_client/lib/src/srp/srp_client.dart
Normal file
142
packages/halkyon_base_client/lib/src/srp/srp_client.dart
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_base_client/src/srp/srp_group.dart';
|
||||
import 'package:halkyon_base_client/src/srp/srp_math.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// The verifier a register keeps for a password, v = g^x mod N, made on the
|
||||
/// device at sign-up from [privateValue] (x), which only the device can make
|
||||
/// (halkyon_keys derives it from the password with Argon2id). The verifier
|
||||
/// can't sign in in the password's place; guessing the password from it
|
||||
/// costs an Argon2id per guess.
|
||||
BigInt srpVerifier(SrpGroup group, BigInt privateValue) =>
|
||||
group.g.modPow(privateValue, group.n);
|
||||
|
||||
/// RFC 5054's x = H(s | H(I | ":" | P)), only to check this implementation
|
||||
/// against RFC 5054's test vectors. Halkyon never hashes a password so: its
|
||||
/// x comes from Argon2id (halkyon_keys), and no password reaches this
|
||||
/// package.
|
||||
@visibleForTesting
|
||||
BigInt rfc5054PrivateValue(
|
||||
SrpGroup group, {
|
||||
required List<int> salt,
|
||||
required String identity,
|
||||
required String password,
|
||||
}) => numberOf(
|
||||
hashOf(group, [
|
||||
salt,
|
||||
hashOf(group, [utf8.encode('$identity:$password')]),
|
||||
]),
|
||||
);
|
||||
|
||||
/// SRP-6a's client side for one sign-in: it makes A ([publicValue]) from a
|
||||
/// fresh secret a, then, given the register's salt and B, proves that it
|
||||
/// knows the password (M1) without sending it, and checks the register's
|
||||
/// proof (M2) in turn. A sign-in that fails starts anew with a new client.
|
||||
final class SrpClient._(
|
||||
/// The group it computes in.
|
||||
final SrpGroup group,
|
||||
final BigInt _secret,
|
||||
) {
|
||||
/// A client with a fresh secret a of 256 bits from [random] (the system's
|
||||
/// secure one by default).
|
||||
factory(SrpGroup group, {Random? random}) =>
|
||||
SrpClient._(group, randomExponent(random ?? Random.secure()));
|
||||
|
||||
/// A client with the secret [a] given, only for RFC 5054's test vectors.
|
||||
@visibleForTesting
|
||||
factory withSecret(SrpGroup group, BigInt a) => SrpClient._(group, a);
|
||||
|
||||
/// A = g^a mod N, sent to the register first.
|
||||
late final BigInt publicValue = group.g.modPow(_secret, group.n);
|
||||
|
||||
/// Answers the register's [serverValue] (B) for the account [identity]
|
||||
/// with the [salt] it gave, from the [privateValue] x the device derived
|
||||
/// from the password: the proof M1 to send, or the refusal of a B that
|
||||
/// would give the password away.
|
||||
SrpClientStep respond({
|
||||
required List<int> identity,
|
||||
required List<int> salt,
|
||||
required BigInt privateValue,
|
||||
required BigInt serverValue,
|
||||
}) {
|
||||
if (!isPublicValue(group, serverValue)) {
|
||||
return const SrpClientRefused(SrpClientRefusal.invalidServerValue);
|
||||
}
|
||||
final scrambler = scramblerOf(group, publicValue, serverValue);
|
||||
if (scrambler == BigInt.zero) {
|
||||
return const SrpClientRefused(SrpClientRefusal.invalidServerValue);
|
||||
}
|
||||
final premasterSecret = premasterSecretOf(
|
||||
privateValue: privateValue,
|
||||
serverValue: serverValue,
|
||||
scrambler: scrambler,
|
||||
);
|
||||
final sessionKey = sessionKeyOf(group, premasterSecret);
|
||||
final proof = clientProofOf(
|
||||
group,
|
||||
identity: identity,
|
||||
salt: salt,
|
||||
clientValue: publicValue,
|
||||
serverValue: serverValue,
|
||||
sessionKey: sessionKey,
|
||||
);
|
||||
return SrpClientProof._(
|
||||
proof,
|
||||
sessionKey,
|
||||
serverProofOf(
|
||||
group,
|
||||
clientValue: publicValue,
|
||||
clientProof: proof,
|
||||
sessionKey: sessionKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// S = (B − k·g^x)^(a + u·x) mod N, exposed for RFC 5054's vectors.
|
||||
@visibleForTesting
|
||||
BigInt premasterSecretOf({
|
||||
required BigInt privateValue,
|
||||
required BigInt serverValue,
|
||||
required BigInt scrambler,
|
||||
}) {
|
||||
final base =
|
||||
(serverValue -
|
||||
multiplierOf(group) * group.g.modPow(privateValue, group.n)) %
|
||||
group.n;
|
||||
return base.modPow(_secret + scrambler * privateValue, group.n);
|
||||
}
|
||||
}
|
||||
|
||||
/// What the client makes of the register's B.
|
||||
sealed class const SrpClientStep();
|
||||
|
||||
/// The proof M1 to send, and what to expect back.
|
||||
final class SrpClientProof._(
|
||||
/// M1, sent to the register.
|
||||
final Uint8List proof,
|
||||
|
||||
/// K, the key both sides now share; never sent.
|
||||
final Uint8List sessionKey,
|
||||
final Uint8List _expectedServerProof,
|
||||
) extends SrpClientStep {
|
||||
/// Whether the register's [serverProof] (M2) shows that it holds the
|
||||
/// verifier: only then is the register the one the account was made with.
|
||||
bool verifiesServer(List<int> serverProof) =>
|
||||
constantTimeEquals(serverProof, _expectedServerProof);
|
||||
}
|
||||
|
||||
/// A B the client refuses to answer.
|
||||
final class const SrpClientRefused(
|
||||
/// Why.
|
||||
final SrpClientRefusal reason,
|
||||
) extends SrpClientStep;
|
||||
|
||||
/// Why the client refused the register's B.
|
||||
enum SrpClientRefusal() {
|
||||
/// B is zero modulo N, or out of range, or makes u zero: answering would
|
||||
/// let the proof be made without the password.
|
||||
invalidServerValue,
|
||||
}
|
||||
55
packages/halkyon_base_client/lib/src/srp/srp_group.dart
Normal file
55
packages/halkyon_base_client/lib/src/srp/srp_group.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import 'package:crypto/crypto.dart';
|
||||
|
||||
/// A group SRP-6a computes in: a safe prime [n], a generator [g] of it, and
|
||||
/// the [hash] every step uses.
|
||||
final class SrpGroup({
|
||||
/// The safe prime N.
|
||||
required final BigInt n,
|
||||
|
||||
/// The generator g.
|
||||
required final BigInt g,
|
||||
|
||||
/// H, SHA-256 in production.
|
||||
required final Hash hash,
|
||||
}) {
|
||||
/// RFC 5054's 3072-bit group (generator 5) with SHA-256: what Halkyon signs
|
||||
/// in with.
|
||||
static final rfc5054Group3072 = SrpGroup(
|
||||
n: BigInt.parse(_n3072, radix: 16),
|
||||
g: BigInt.from(5),
|
||||
hash: sha256,
|
||||
);
|
||||
|
||||
/// RFC 5054's 1024-bit group (generator 2) with SHA-1: only for the test
|
||||
/// vectors of RFC 5054's Appendix B, never to sign in with.
|
||||
static final rfc5054TestVectorGroup = SrpGroup(
|
||||
n: BigInt.parse(_n1024, radix: 16),
|
||||
g: BigInt.two,
|
||||
hash: sha1,
|
||||
);
|
||||
|
||||
/// How many bytes N takes: what PAD() fills to.
|
||||
int get length => (n.bitLength + 7) ~/ 8;
|
||||
}
|
||||
|
||||
/// RFC 5054, Appendix A, the 3072-bit group's N.
|
||||
const _n3072 =
|
||||
'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74'
|
||||
'020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437'
|
||||
'4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED'
|
||||
'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05'
|
||||
'98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB'
|
||||
'9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B'
|
||||
'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718'
|
||||
'3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33'
|
||||
'A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7'
|
||||
'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864'
|
||||
'D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2'
|
||||
'08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF';
|
||||
|
||||
/// RFC 5054, Appendix A, the 1024-bit group's N.
|
||||
const _n1024 =
|
||||
'EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576'
|
||||
'D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD1'
|
||||
'5DC7D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC'
|
||||
'68EDBC3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3';
|
||||
129
packages/halkyon_base_client/lib/src/srp/srp_math.dart
Normal file
129
packages/halkyon_base_client/lib/src/srp/srp_math.dart
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_base_client/src/srp/srp_group.dart';
|
||||
|
||||
/// The arithmetic both sides of SRP-6a share, as the server's half has it
|
||||
/// too (halkyon_base_server's `srp_math.dart`, which must compute alike: an
|
||||
/// interoperability test there holds them to each other).
|
||||
///
|
||||
/// Byte strings are unsigned and big-endian; PAD(x) is x filled with leading
|
||||
/// zeros to the length of N.
|
||||
|
||||
/// [value] as the fewest unsigned big-endian bytes (one zero byte for zero).
|
||||
Uint8List bytesOf(BigInt value) {
|
||||
if (value.isNegative) {
|
||||
throw ArgumentError.value(value, 'value', 'must not be negative');
|
||||
}
|
||||
var hex = value.toRadixString(16);
|
||||
if (hex.length.isOdd) {
|
||||
hex = '0$hex';
|
||||
}
|
||||
return Uint8List.fromList([
|
||||
for (var i = 0; i < hex.length; i += 2)
|
||||
int.parse(hex.substring(i, i + 2), radix: 16),
|
||||
]);
|
||||
}
|
||||
|
||||
/// PAD([value]): its bytes filled with leading zeros to [length].
|
||||
Uint8List padded(BigInt value, int length) {
|
||||
final bytes = bytesOf(value);
|
||||
if (bytes.length > length) {
|
||||
throw ArgumentError.value(value, 'value', 'is longer than $length bytes');
|
||||
}
|
||||
return Uint8List(length)..setRange(length - bytes.length, length, bytes);
|
||||
}
|
||||
|
||||
/// The unsigned big-endian number [bytes] stand for.
|
||||
BigInt numberOf(List<int> bytes) {
|
||||
var value = BigInt.zero;
|
||||
for (final byte in bytes) {
|
||||
value = (value << 8) | BigInt.from(byte);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// H over [parts], one after the other.
|
||||
Uint8List hashOf(SrpGroup group, Iterable<List<int>> parts) =>
|
||||
Uint8List.fromList(
|
||||
group.hash.convert([for (final part in parts) ...part]).bytes,
|
||||
);
|
||||
|
||||
/// k = H(N | PAD(g)).
|
||||
BigInt multiplierOf(SrpGroup group) =>
|
||||
numberOf(hashOf(group, [bytesOf(group.n), padded(group.g, group.length)]));
|
||||
|
||||
/// u = H(PAD(A) | PAD(B)).
|
||||
BigInt scramblerOf(SrpGroup group, BigInt clientValue, BigInt serverValue) =>
|
||||
numberOf(
|
||||
hashOf(group, [
|
||||
padded(clientValue, group.length),
|
||||
padded(serverValue, group.length),
|
||||
]),
|
||||
);
|
||||
|
||||
/// K = H(PAD(S)), the key both sides end up with.
|
||||
Uint8List sessionKeyOf(SrpGroup group, BigInt premasterSecret) =>
|
||||
hashOf(group, [padded(premasterSecret, group.length)]);
|
||||
|
||||
/// M1 = H(H(N) xor H(g) | H(I) | s | PAD(A) | PAD(B) | K): the client's
|
||||
/// proof that it knows the password.
|
||||
Uint8List clientProofOf(
|
||||
SrpGroup group, {
|
||||
required List<int> identity,
|
||||
required List<int> salt,
|
||||
required BigInt clientValue,
|
||||
required BigInt serverValue,
|
||||
required List<int> sessionKey,
|
||||
}) {
|
||||
final hashOfN = hashOf(group, [bytesOf(group.n)]);
|
||||
final hashOfG = hashOf(group, [bytesOf(group.g)]);
|
||||
return hashOf(group, [
|
||||
[for (var i = 0; i < hashOfN.length; i++) hashOfN[i] ^ hashOfG[i]],
|
||||
hashOf(group, [identity]),
|
||||
salt,
|
||||
padded(clientValue, group.length),
|
||||
padded(serverValue, group.length),
|
||||
sessionKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/// M2 = H(PAD(A) | M1 | K): the server's proof that it holds the verifier.
|
||||
Uint8List serverProofOf(
|
||||
SrpGroup group, {
|
||||
required BigInt clientValue,
|
||||
required List<int> clientProof,
|
||||
required List<int> sessionKey,
|
||||
}) => hashOf(group, [
|
||||
padded(clientValue, group.length),
|
||||
clientProof,
|
||||
sessionKey,
|
||||
]);
|
||||
|
||||
/// Whether a public value lies in 1 … N − 1: zero (mod N) would let the
|
||||
/// other side's key be known without the password.
|
||||
bool isPublicValue(SrpGroup group, BigInt value) =>
|
||||
value > BigInt.zero && value < group.n;
|
||||
|
||||
/// Whether [a] and [b] are equal, in a time that depends only on their
|
||||
/// lengths, which are public.
|
||||
bool constantTimeEquals(List<int> a, List<int> b) {
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
var difference = 0;
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
difference |= a[i] ^ b[i];
|
||||
}
|
||||
return difference == 0;
|
||||
}
|
||||
|
||||
/// A fresh secret exponent of 256 bits from [random], never zero.
|
||||
BigInt randomExponent(Random random) {
|
||||
while (true) {
|
||||
final value = numberOf([for (var i = 0; i < 32; i++) random.nextInt(256)]);
|
||||
if (value != BigInt.zero) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/halkyon_base_client/lib/srp.dart
Normal file
16
packages/halkyon_base_client/lib/srp.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/// SRP-6a's client side (ADR 0017): the device proves it knows the password
|
||||
/// without sending it, and checks that the register holds the verifier made
|
||||
/// at sign-up. Hand-written beside the generated client: Serverpod's
|
||||
/// generator only ever writes and cleans `lib/src/protocol/`.
|
||||
library;
|
||||
|
||||
export 'src/srp/srp_client.dart'
|
||||
show
|
||||
SrpClient,
|
||||
SrpClientProof,
|
||||
SrpClientRefusal,
|
||||
SrpClientRefused,
|
||||
SrpClientStep,
|
||||
rfc5054PrivateValue,
|
||||
srpVerifier;
|
||||
export 'src/srp/srp_group.dart';
|
||||
|
|
@ -2,7 +2,7 @@ name: halkyon_base_client
|
|||
description: >-
|
||||
The client side of halkyon_base, generated by Serverpod: what a Halkyon
|
||||
app calls to sign in, to manage its account, and to learn an instance's
|
||||
basics.
|
||||
basics; and SRP-6a's client half, written by hand.
|
||||
version: 0.1.0
|
||||
publish_to: none
|
||||
|
||||
|
|
@ -12,5 +12,11 @@ environment:
|
|||
resolution: workspace
|
||||
|
||||
dependencies:
|
||||
# SRP-6a's hashes (lib/src/srp/, hand-written beside the generated code).
|
||||
crypto: ^3.0.7
|
||||
meta: ^1.16.0
|
||||
serverpod_auth_idp_client: 4.0.4
|
||||
serverpod_client: 4.0.4
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.31.1
|
||||
|
|
|
|||
188
packages/halkyon_base_client/test/srp_client_test.dart
Normal file
188
packages/halkyon_base_client/test/srp_client_test.dart
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue