Description
The browser ECDH backend rounds length up to a whole number of bytes before calling SubtleCrypto.deriveBits, but it does not validate the value first. Near the WebIDL unsigned long boundary, the rounded value wraps before the browser performs the curve-specific length check.
This creates backend-dependent behavior for inputs that are outside the documented ECDH limit:
- The native backend rejects them.
- Chrome under both dart2js and dart2wasm can throw an unrelated
StateError, return an empty result, or return a one-bit result.
Reproduction
import 'package:webcrypto/webcrypto.dart';
Future<void> main() async {
final alice = await EcdhPrivateKey.generateKey(EllipticCurve.p256);
final bob = await EcdhPrivateKey.generateKey(EllipticCurve.p256);
for (final length in <int>[
-1,
0xfffffff9,
0xffffffff,
0x100000000,
0x100000001,
]) {
try {
final secret = await alice.privateKey.deriveBits(
length,
bob.publicKey,
);
print('$length: accepted with ${secret.length} bytes');
} catch (error) {
print('$length: ${error.runtimeType}: $error');
}
}
}
Native output:
-1: ArgumentError
4294967289: OperationError
4294967295: OperationError
4294967296: OperationError
4294967297: OperationError
Chrome output under both dart2js and dart2wasm:
-1: StateError: Bad state: No element
4294967289: StateError: Bad state: No element
4294967295: StateError: Bad state: No element
4294967296: accepted with 0 bytes
4294967297: accepted with 1 bytes
Expected behavior
Invalid ECDH output lengths should be rejected consistently before browser numeric conversion and before indexing the derived result. In particular, values outside the selected curve's maximum must not wrap into an empty or one-bit derivation, and invalid negative values must not surface as StateError.
Valid zero-length and non-byte-aligned derivations should continue to work.
Cause
The shared public method forwards length without validation. The browser implementation then calculates:
final lengthInBytes = (length / 8).ceil();
and passes lengthInBytes * 8 to SubtleCrypto.deriveBits. Values such as 0xfffffff9 through 0xffffffff round to 0x100000000, which WebIDL converts to zero for the browser call. The wrapper then accesses derived.last while applying its non-byte-aligned mask, causing StateError. Values at and above 0x100000000 can similarly wrap to small successful derivations.
The native implementation validates negative and curve-specific maximum lengths before deriving, so it does not exhibit this behavior.
Suggested fix
- Validate ECDH
length before the browser backend rounds it or passes it through WebIDL conversion.
- Preserve the curve-specific maximums: 256 bits for P-256, 384 bits for P-384, and 528 bits for P-521.
- Add shared regression coverage for negative values, the first value above each curve maximum,
0xfffffff9, 0xffffffff, 0x100000000, and 0x100000001.
- Run the regression coverage on native, Chrome/dart2js, and Chrome/dart2wasm.
Description
The browser ECDH backend rounds
lengthup to a whole number of bytes before callingSubtleCrypto.deriveBits, but it does not validate the value first. Near the WebIDLunsigned longboundary, the rounded value wraps before the browser performs the curve-specific length check.This creates backend-dependent behavior for inputs that are outside the documented ECDH limit:
StateError, return an empty result, or return a one-bit result.Reproduction
Native output:
Chrome output under both dart2js and dart2wasm:
Expected behavior
Invalid ECDH output lengths should be rejected consistently before browser numeric conversion and before indexing the derived result. In particular, values outside the selected curve's maximum must not wrap into an empty or one-bit derivation, and invalid negative values must not surface as
StateError.Valid zero-length and non-byte-aligned derivations should continue to work.
Cause
The shared public method forwards
lengthwithout validation. The browser implementation then calculates:and passes
lengthInBytes * 8toSubtleCrypto.deriveBits. Values such as0xfffffff9through0xffffffffround to0x100000000, which WebIDL converts to zero for the browser call. The wrapper then accessesderived.lastwhile applying its non-byte-aligned mask, causingStateError. Values at and above0x100000000can similarly wrap to small successful derivations.The native implementation validates negative and curve-specific maximum lengths before deriving, so it does not exhibit this behavior.
Suggested fix
lengthbefore the browser backend rounds it or passes it through WebIDL conversion.0xfffffff9,0xffffffff,0x100000000, and0x100000001.